Compare commits

...

68 Commits

Author SHA1 Message Date
ed
987caec15d v1.9.28 2023-12-31 18:49:42 +00:00
ed
ab40ff5051 add permission "A" (alias of "rwmda."); closes #70 2023-12-31 18:20:24 +00:00
ed
bed133d3dd pad log source when logging to file too 2023-12-31 17:21:02 +00:00
ed
829c8fca96 curl/CLI-friendly 403/404 2023-12-31 17:20:45 +00:00
ed
5b26ab0096 add option to specify default num parallel uploads 2023-12-28 01:41:17 +01:00
ed
39554b4bc3 guard against unintended access if user-db is corrupted 2023-12-24 16:12:18 +01:00
ed
97d9c149f1 IdP config draft (#62) 2023-12-24 13:46:26 +01:00
ed
59688bc8d7 * rename hdr-au-usr to idp-h-usr
* ensure lowercase idp-h-*, xff-hdr
* more macos support in tooling
2023-12-24 13:46:12 +01:00
ed
a18f63895f fix resource leak on macos 2023-12-21 00:48:51 +01:00
ed
27433d6214 remove fedora/pypi-copr mention because copr has died;
https://github.com/fedora-copr/copr/issues/3056
2023-12-20 22:35:52 +00:00
ed
374c535cfa fix cors-checker so it behaves like the readme says;
any custom header (`pw` in our case) is sufficient validation
2023-12-20 20:03:08 +00:00
ed
ac7815a0ae ensure file can be opened before replying 200 and...
* make gen_tree 0.1% faster
* improve filekey warning message
* fix oversight in 0c50ea1757
* support `--xdev` on windows (the python docs mention that os.scandir
   doesn't assign st_ino, st_dev and st_nlink on win but i can't read)
2023-12-20 01:07:45 +00:00
ed
0c50ea1757 list dotfiles only for specific volumes or users (#66):
* permission `.` grants dotfile visibility if user has `r` too
* `-ed` will grant dotfiles to all `r` accounts (same as before)
* volflag `dots` likewise

also drops compatibility for pre-0.12.0 `-v` syntax
(`-v .::red` will no longer translate to `-v .::r,ed`)
2023-12-16 15:38:48 +00:00
ed
c057c5e8e8 extend --th-covers with dotfiles; closes #67 2023-12-14 10:53:15 +00:00
ed
46d667716e support python 3.15 2023-12-14 10:49:10 +00:00
ed
cba2e10d29 cleanup 2023-12-14 10:47:52 +00:00
ed
b1693f95cb alternative fedora packages for when copr breaks 2023-12-09 02:05:06 +00:00
ed
3f00073256 update pkgs to 1.9.27 2023-12-08 21:58:59 +00:00
ed
d15000062d v1.9.27 2023-12-08 21:33:12 +00:00
ed
6cb3b35a54 fix #65 (symlinks die when moved) 2023-12-08 21:28:20 +00:00
ed
b4031e8d43 forgot to bump this... oh well, at least the exe is correct 2023-12-08 02:16:40 +00:00
ed
a3ca0638cb update pkgs to 1.9.26 2023-12-08 02:10:06 +00:00
ed
a360ac29da v1.9.26 2023-12-08 01:36:01 +00:00
ed
9672b8c9b3 ensure nested symlinks are not broken during deletes;
when moving/deleting a file, all symlinked dupes are verified to ensure
this action does not break any symlinks, however it did this by checking
the realpath of each link. This was not good enough, since the deleted
file may be a part of a series of nested symlinks

this situation occurs because the deduper tries to keep relative
symlinks as close as possible, only traversing into parent/sibling
folders as required, which can lead to several levels of nested links
2023-12-08 01:11:03 +00:00
ed
e70ecd98ef don't freak out when deleting a broken symlink,
also invoke the hooks with the corret lastmod time
2023-12-08 01:01:10 +00:00
ed
5f7ce78d7f avoid duplicate database entries when replacing files,
either from --daw, or by using u2c with --dr
2023-12-08 01:00:01 +00:00
ed
2077dca66f u2c: when deleting from server, heed request size limit 2023-12-08 00:54:57 +00:00
ed
91f010290c improve --help descriptions 2023-12-03 02:35:38 +00:00
ed
395e3386b7 mention --help for features not documented in readme
plus some small fixes to the packaging section
2023-12-02 23:32:31 +00:00
ed
a1dce0f24e update pkgs to 1.9.25 2023-12-01 23:51:35 +00:00
ed
c7770904e6 v1.9.25 2023-12-01 23:26:16 +00:00
ed
1690889ed8 remember scroll position when leaving the textfile viewer 2023-12-01 23:15:48 +00:00
ed
842817d9e3 improve handling of malicious clients;
* start banning malicious clients according to --ban-422
* reply with a blank 500 to stop firefox from retrying like 20 times
* allow Cc's in a few specific URL params (filenames, dirnames)
2023-12-01 23:08:16 +00:00
ed
5fc04152bd also handle NumpadEnter 2023-12-01 21:10:51 +00:00
ed
1be85bdb26 fix modal focus even more (now works on phones too) 2023-12-01 21:02:05 +00:00
ed
2eafaa88a2 update pkgs to 1.9.24 2023-12-01 02:16:24 +00:00
ed
900cc463c3 v1.9.24 2023-12-01 02:10:20 +00:00
ed
97b999c463 update pkgs to 1.9.23 2023-12-01 01:54:23 +00:00
ed
a7cef91b8b v1.9.23 2023-12-01 00:39:49 +00:00
ed
a4a112c0ee update pkgs to 1.9.22 2023-12-01 01:14:18 +00:00
ed
e6bcee28d6 v1.9.22 2023-12-01 00:31:02 +00:00
ed
626b5770a5 add --ftp-ipa 2023-11-30 23:36:46 +00:00
ed
c2f92cacc1 mention the new auth feature 2023-11-30 23:01:05 +00:00
ed
4f8a1f5f6a allow free text selection in modals by deferring focus 2023-11-30 22:41:16 +00:00
ed
4a98b73915 fix a bug previouly concealed by window.event;
hitting enter would clear out an entire chain of modals,
because the event didn't get consumed like it should,
so let's make double sure that will be the case
2023-11-30 22:40:30 +00:00
ed
00812cb1da new option --ipa; client IP allowlist:
connections from outside the specified list of IP prefixes are rejected
(docker-friendly alternative to -i 127.0.0.1)

also mkdir any missing folders when logging to file
2023-11-30 20:45:43 +00:00
ed
16766e702e add basic-docker-compose (#59) 2023-11-30 20:14:38 +00:00
ed
5e932a9504 hilight metavars in help text 2023-11-30 18:19:34 +00:00
ed
ccab44daf2 initial support for identity providers (#62):
add argument --hdr-au-usr which specifies a HTTP header to read
usernames from; entirely bypasses copyparty's password checks
for http/https clients (ftp/smb are unaffected)

users must exist in the copyparty config, passwords can be whatever

just the first step but already a bit useful on its own,
more to come in a few months
2023-11-30 18:18:47 +00:00
ed
8c52b88767 make linters happier 2023-11-30 17:33:07 +00:00
ed
c9fd26255b support environment variables mostly everywhere,
useful for docker/systemd stuff

also makes logfiles flush to disk per line by default;
can be disabled for a small performance gain with --no-logflush
2023-11-30 10:22:52 +00:00
ed
0b9b8dbe72 systemd: get rid of nftables portforwarding;
suggest letting copyparty bind 80/443 itself because nft hard
2023-11-30 10:13:14 +00:00
ed
b7723ac245 rely on filekeys for album-art over bluetooth;
will probably fail when some devices (sup iphone) stream to car stereos
but at least passwords won't end up somewhere unexpected this way
(plus, the js no longer uses the jank url to request waveforms)
2023-11-29 23:20:59 +00:00
ed
35b75c3db1 avoid palemoon bug on dragging a text selection;
"permission denied to access property preventDefault"
2023-11-26 20:22:59 +00:00
ed
f902779050 avoid potential dom confusion (ie8 is already no-js) 2023-11-26 20:08:52 +00:00
ed
fdddd36a5d update pkgs to 1.9.21 2023-11-25 14:48:41 +00:00
ed
c4ba123779 v1.9.21 2023-11-25 14:17:58 +00:00
ed
72e355eb2c prisonparty: prevent overlapping setup/teardown 2023-11-25 14:03:41 +00:00
ed
43d409a5d9 prisonparty accepts user/group names 2023-11-25 13:40:21 +00:00
ed
b1fffc2246 open textfiles inline in grid-view, closes #63;
also fix the Y hotkey (which converts all links in the list-view into
download links), making that apply to the grid-view as well
2023-11-25 13:09:12 +00:00
ed
edd3e53ab3 prisonparty: support zfs-ubuntu
* when bind-mounting, resolve any symlinks ($v/) and read target inode;
   for example merged /bin and /usr/bin
* add failsafe in case this test should break in new exciting ways;
   inspect `mount` for any instances of the jailed path
   (not /proc/mounts since that has funny space encoding)
* unmount in a while-loop because xargs freaks out if one of them fail
   * and systemd doesn't give us a /dev/stderr to write to anyways
2023-11-25 02:16:48 +00:00
ed
aa0b119031 update pkgs to 1.9.20 2023-11-21 23:44:56 +00:00
ed
eddce00765 v1.9.20 2023-11-21 23:25:41 +00:00
ed
6f4bde2111 fix infinite backspin on "previous track";
when playing the first track in a folder and hitting the previous track
button, it would keep switching through the previous folders inifinitely
2023-11-21 23:23:51 +00:00
ed
f3035e8869 clear load-more buttons upon navigation (thx icxes) 2023-11-21 22:53:46 +00:00
ed
a9730499c0 don't suggest loading more search results beyond server cap 2023-11-21 22:38:35 +00:00
ed
b66843efe2 reduce cpu priority of ffmpeg, hooks, parsers 2023-11-21 22:21:33 +00:00
ed
cc1aaea300 update pkgs to 1.9.19 2023-11-19 12:45:32 +00:00
53 changed files with 1623 additions and 756 deletions

View File

@@ -26,6 +26,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [FAQ](#FAQ) - "frequently" asked questions
* [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions
* [shadowing](#shadowing) - hiding specific subfolders
* [dotfiles](#dotfiles) - unix-style hidden files/folders
* [the browser](#the-browser) - accessing a copyparty server using a web-browser
* [tabs](#tabs) - the main tabs in the ui
* [hotkeys](#hotkeys) - the browser has the following hotkeys
@@ -67,6 +68,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
* [themes](#themes)
* [complete examples](#complete-examples)
@@ -74,7 +76,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [prometheus](#prometheus) - metrics/stats can be enabled
* [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
* [fedora package](#fedora-package) - now [available on copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/)
* [fedora package](#fedora-package) - currently **NOT** available on [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/)
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module)
* [browser support](#browser-support) - TLDR: yes
@@ -111,7 +113,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
* or install through pypi: `python3 -m pip install --user -U copyparty`
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
* or install [on arch](#arch-package) [on fedora](#fedora-package) [on NixOS](#nixos-module) [through nix](#nix-package)
* or install [on arch](#arch-package) [on NixOS](#nixos-module) [through nix](#nix-package)
* or if you are on android, [install copyparty in termux](#install-on-android)
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
* docker has all deps built-in, so skip this step:
@@ -119,8 +121,8 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
enable thumbnails (images/audio/video), media indexing, and audio transcoding by installing some recommended deps:
* **Alpine:** `apk add py3-pillow ffmpeg`
* **Debian:** `apt install python3-pil ffmpeg`
* **Fedora:** `dnf install python3-pillow ffmpeg`
* **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
* **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg`
* **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
* **MacOS:** `port install py-Pillow ffmpeg`
* **MacOS** (alternative): `brew install pillow ffmpeg`
@@ -147,9 +149,10 @@ you may also want these, especially on servers:
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service (see guide inside)
* [contrib/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)
* [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo
* [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)
* [nixos module](#nixos-module) to run copyparty on NixOS hosts
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
```
@@ -340,7 +343,7 @@ upgrade notes
* yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py)
* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion
```bash
* ```bash
_| _ __ _ _|_
(_| (_) | | (_) |_
```
@@ -366,10 +369,12 @@ permissions:
* `w` (write): upload files, move files *into* this folder
* `m` (move): move files/folders *from* this folder
* `d` (delete): delete files/folders
* `.` (dots): user can ask to show dotfiles in directory listings
* `g` (get): only download files, cannot see folder contents or zip/tar
* `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below)
* `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html
* `a` (admin): can see upload time, uploader IPs, config-reload
* `A` ("all"): same as `rwmda.` (read/write/move/delete/admin/dotfiles)
examples:
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
@@ -397,6 +402,17 @@ hiding specific subfolders by mounting another volume on top of them
for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
## dotfiles
unix-style hidden files/folders by starting the name with a dot
anyone can access these if they know the name, but they normally don't appear in directory listings
a client can request to see dotfiles in directory listings if global option `-ed` is specified, or the volume has volflag `dots`, or the user has permission `.`
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
# the browser
accessing a copyparty server using a web-browser
@@ -509,7 +525,7 @@ it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video f
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
* and, if you enable [file indexing](#file-indexing), all remaining folders will also get thumbnails (as long as they contain any pics at all)
* and, if you enable [file indexing](#file-indexing), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all)
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
* indicated by the audio files having the ▶ icon instead of 💾
@@ -537,7 +553,7 @@ select which type of archive you want in the `[⚙️] config` tab:
* gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9`
* xz default level is `1` (0=fast, 9=best), change with `?tar=xz:9`
* bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9`
* hidden files (dotfiles) are excluded unless `-ed`
* hidden files ([dotfiles](#dotfiles)) are excluded unless account is allowed to list them
* `up2k.db` and `dir.txt` is always excluded
* bsdtar supports streaming unzipping: `curl foo?zip=utf8 | bsdtar -xv`
* good, because copyparty's zip is faster than tar on small files
@@ -822,6 +838,9 @@ using arguments or config files, or a mix of both:
* or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume
* changes to the `[global]` config section requires a restart to take effect
**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
## zeroconf
@@ -1193,6 +1212,17 @@ redefine behavior with plugins ([examples](./bin/handlers/))
replace 404 and 403 errors with something completely different (that's it for now)
## identity providers
replace copyparty passwords with oauth and such
work is [ongoing](https://github.com/9001/copyparty/issues/62) to support authenticating / authorizing users based on a separate authentication proxy, which makes it possible to support oauth, single-sign-on, etc.
it is currently possible to specify `--idp-h-usr x-username`; copyparty will then skip password validation and blindly trust the username specified in the `X-Username` request header
the remaining stuff (accepting user groups through another header, creating volumes on the fly) are still to-do; configuration will probably [look like this](./docs/examples/docker/idp/copyparty.conf)
## hiding from google
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
@@ -1252,8 +1282,8 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
* anyone can upload, and receive "secret" links for each upload they do:
`python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8`
* anyone can browse, only `kevin` (password `okgo`) can upload/move/delete files:
`python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:rwmd,kevin`
* anyone can browse (`r`), only `kevin` (password `okgo`) can upload/move/delete (`A`) files:
`python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:A,kevin`
* read-only music server:
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
@@ -1353,23 +1383,29 @@ note: the following metrics are counted incorrectly if multiprocessing is enable
the party might be closer than you think
if your distro/OS is not mentioned below, there might be some hints in the [«on servers»](#on-servers) section
## arch package
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
it comes with a [systemd service](./contrib/package/arch/copyparty.service) and expects to find one or more [config files](./docs/example.conf) in `/etc/copyparty.d/`
## fedora package
now [available on copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) , maintained autonomously -- [track record](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/package/python-copyparty/) seems OK
currently **NOT** available on [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) , fedora is having issues with their build servers and won't be fixed for several months
if you previously installed copyparty from copr, you may run one of the following commands to upgrade to a more recent version:
```bash
dnf copr enable @copr/PyPI
dnf install python3-copyparty # just a minimal install, or...
dnf install python3-{copyparty,pillow,argon2-cffi,pyftpdlib,pyOpenSSL} ffmpeg-free # with recommended deps
dnf install https://ocv.me/copyparty/fedora/37/python3-copyparty.fc37.noarch.rpm
dnf install https://ocv.me/copyparty/fedora/38/python3-copyparty.fc38.noarch.rpm
dnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm
```
this *may* also work on RHEL but [I'm not paying IBM to verify that](https://www.jeffgeerling.com/blog/2023/dear-red-hat-are-you-dumb)
to run copyparty as a service, use the [systemd service scripts](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd), just replace `/usr/bin/python3 /usr/local/bin/copyparty-sfx.py` with `/usr/bin/copyparty`
## nix package
@@ -1505,7 +1541,7 @@ TLDR: yes
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
* internet explorer 6 to 8 behave the same
* internet explorer 6 through 8 behave the same
* firefox 52 and chrome 49 are the final winxp versions
* `*1` yes, but extremely slow (ie10: `1 MiB/s`, ie11: `270 KiB/s`)
* `*3` iOS 11 and newer, opus only, and requires FFmpeg on the server

View File

@@ -207,7 +207,7 @@ def examples():
def main():
global NC, BY_PATH
global NC, BY_PATH # pylint: disable=global-statement
os.system("")
print()
@@ -282,7 +282,8 @@ def main():
if ver == "corrupt":
die("{} database appears to be corrupt, sorry")
if ver < DB_VER1 or ver > DB_VER2:
iver = int(ver)
if iver < DB_VER1 or iver > DB_VER2:
m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first"
die(m)

View File

@@ -53,7 +53,13 @@ from urllib.parse import unquote_to_bytes as unquote
WINDOWS = sys.platform == "win32"
MACOS = platform.system() == "Darwin"
UTC = timezone.utc
info = log = dbg = None
def print(*args, **kwargs):
try:
builtins.print(*list(args), **kwargs)
except:
builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
print(
@@ -65,6 +71,13 @@ print(
)
def null_log(msg):
pass
info = log = dbg = null_log
try:
from fuse import FUSE, FuseOSError, Operations
except:
@@ -84,13 +97,6 @@ except:
raise
def print(*args, **kwargs):
try:
builtins.print(*list(args), **kwargs)
except:
builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
def termsafe(txt):
try:
return txt.encode(sys.stdout.encoding, "backslashreplace").decode(
@@ -119,10 +125,6 @@ def fancy_log(msg):
print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="")
def null_log(msg):
pass
def hexler(binary):
return binary.replace("\r", "\\r").replace("\n", "\\n")
return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary])

View File

@@ -12,13 +12,13 @@ done
help() { cat <<'EOF'
usage:
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]
./prisonparty.sh <ROOTDIR> <USER|UID> <GROUP|GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...]
example:
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd
./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd
example for running straight from source (instead of using an sfx):
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
note that if you have python modules installed as --user (such as bpm/key detectors),
you should add /home/foo/.local as a VOLDIR
@@ -28,6 +28,16 @@ exit 1
}
errs=
for c in awk chroot dirname getent lsof mknod mount realpath sed sort stat uniq; do
command -v $c >/dev/null || {
echo ERROR: command not found: $c
errs=1
}
done
[ $errs ] && exit 1
# read arguments
trap help EXIT
jail="$(realpath "$1")"; shift
@@ -58,11 +68,18 @@ cpp="$1"; shift
}
trap - EXIT
usr="$(getent passwd $uid | cut -d: -f1)"
[ "$usr" ] || { echo "ERROR invalid username/uid $uid"; exit 1; }
uid="$(getent passwd $uid | cut -d: -f3)"
grp="$(getent group $gid | cut -d: -f1)"
[ "$grp" ] || { echo "ERROR invalid groupname/gid $gid"; exit 1; }
gid="$(getent group $gid | cut -d: -f3)"
# debug/vis
echo
echo "chroot-dir = $jail"
echo "user:group = $uid:$gid"
echo "user:group = $uid:$gid ($usr:$grp)"
echo " copyparty = $cpp"
echo
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
@@ -80,34 +97,39 @@ jail="${jail%/}"
# bind-mount system directories and volumes
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
while IFS= read -r v; do
[ -e "$v" ] || {
printf '\033[1;31mfolder does not exist:\033[0m %s\n' "$v"
continue
}
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
# echo "v [$v] i1 [$i1] i2 [$i2]"
i1=$(stat -c%D.%i "$v/" 2>/dev/null || echo a)
i2=$(stat -c%D.%i "$jail$v/" 2>/dev/null || echo b)
[ $i1 = $i2 ] && continue
mount | grep -qF " $jail$v " && echo wtf $i1 $i2 $v && continue
mkdir -p "$jail$v"
mount --bind "$v" "$jail$v"
done
rmdir "$jail/.prisonlock" || true
cln() {
rv=$?
wait -f -p rv $p || true
trap - EXIT
wait -f -n $p && rv=0 || rv=$?
cd /
echo "stopping chroot..."
lsof "$jail" | grep -F "$jail" &&
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
lsof "$jail" 2>/dev/null | grep -F "$jail" &&
echo "chroot is in use; will not unmount" ||
{
mount | grep -F " on $jail" |
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
LC_ALL=C sort -r | while IFS= read -r v; do
umount "$v" && echo "umount OK: $v"
done
}
rmdir "$jail/.prisonlock" || true
exit $rv
}
trap cln EXIT
@@ -128,8 +150,8 @@ chmod 777 "$jail/tmp"
# run copyparty
export HOME=$(getent passwd $uid | cut -d: -f6)
export USER=$(getent passwd $uid | cut -d: -f1)
export HOME="$(getent passwd $uid | cut -d: -f6)"
export USER="$usr"
export LOGNAME="$USER"
#echo "pybin [$pybin]"
#echo "pyarg [$pyarg]"
@@ -137,5 +159,5 @@ export LOGNAME="$USER"
chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" &
p=$!
trap 'kill -USR1 $p' USR1
trap 'kill $p' INT TERM
trap 'trap - INT TERM; kill $p' INT TERM
wait

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "1.11"
S_BUILD_DT = "2023-11-11"
S_VERSION = "1.12"
S_BUILD_DT = "2023-12-08"
"""
u2c.py: upload to copyparty
@@ -105,8 +105,8 @@ class File(object):
# set by handshake
self.recheck = False # duplicate; redo handshake after all files done
self.ucids = [] # type: list[str] # chunks which need to be uploaded
self.wark = None # type: str
self.url = None # type: str
self.wark = "" # type: str
self.url = "" # type: str
self.nhs = 0
# set by upload
@@ -223,6 +223,7 @@ class MTHash(object):
def hash_at(self, nch):
f = self.f
assert f
ofs = ofs0 = nch * self.csz
hashobj = hashlib.sha512()
chunk_sz = chunk_rem = min(self.csz, self.sz - ofs)
@@ -463,7 +464,7 @@ def quotep(btxt):
if not PY2:
quot1 = quot1.encode("ascii")
return quot1.replace(b" ", b"+")
return quot1.replace(b" ", b"+") # type: ignore
# from copyparty/util.py
@@ -500,7 +501,7 @@ def up2k_chunksize(filesize):
# mostly from copyparty/up2k.py
def get_hashlist(file, pcb, mth):
# type: (File, any, any) -> None
# type: (File, Any, Any) -> None
"""generates the up2k hashlist from file contents, inserts it into `file`"""
chunk_sz = up2k_chunksize(file.size)
@@ -906,12 +907,23 @@ class Ctl(object):
dp = os.path.join(top, rd)
lnodes = set(os.listdir(dp))
bnames = [x for x in ls if x not in lnodes]
if bnames:
vpath = self.ar.url.split("://")[-1].split("/", 1)[-1]
names = [x.decode("utf-8", "replace") for x in bnames]
locs = [vpath + srd + "/" + x for x in names]
print("DELETING ~{0}/#{1}".format(srd, len(names)))
req_ses.post(self.ar.url + "?delete", json=locs)
vpath = self.ar.url.split("://")[-1].split("/", 1)[-1]
names = [x.decode("utf-8", "replace") for x in bnames]
locs = [vpath + srd + "/" + x for x in names]
while locs:
req = locs
while req:
print("DELETING ~%s/#%s" % (srd, len(req)))
r = req_ses.post(self.ar.url + "?delete", json=req)
if r.status_code == 413 and "json 2big" in r.text:
print(" (delete request too big; slicing...)")
req = req[: len(req) // 2]
continue
elif not r:
t = "delete request failed: %r %s"
raise Exception(t % (r, r.text))
break
locs = locs[len(req) :]
if isdir:
continue
@@ -1116,7 +1128,7 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\.hist/.*'")
ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'")
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
ap.add_argument("--version", action="store_true", help="show version and exit")

View File

@@ -66,7 +66,7 @@ def main():
ofs = ln.find("{")
j = json.loads(ln[ofs:])
except:
pass
continue
w = j["wark"]
if db.execute("select w from up where w = ?", (w,)).fetchone():

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty
pkgver="1.9.18"
pkgver="1.9.27"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("2f89ace0c5bc6a6990e85b6aa635c96dac16a6b399e4c9d040743695f35edb52")
sha256sums=("0bcf9362bc1bd9c85c228312c8341fcb9a37e383af6d8bee123e3a84e66394be")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,11 +1,11 @@
# this will start `/usr/bin/copyparty-sfx.py`
# in a chroot, preventing accidental access elsewhere
# and read config from `/etc/copyparty.d/*.conf`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty.d/*.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `1000` and `--`
# by listing them between the last `cpp` and `--`
#
# `1000 1000` = what user to run copyparty as
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
@@ -24,7 +24,9 @@ ExecReload=/bin/kill -s USR1 $MAINPID
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# run copyparty
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail 1000 1000 /etc/copyparty.d -- \
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail cpp cpp \
/etc/copyparty.d \
-- \
/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
[Install]

View File

@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.9.18/copyparty-sfx.py",
"version": "1.9.18",
"hash": "sha256-Qun8qzlThNjO+DUH33p7pkPXAJq20B6tEVbR+I4d0Uc="
"url": "https://github.com/9001/copyparty/releases/download/v1.9.27/copyparty-sfx.py",
"version": "1.9.27",
"hash": "sha256-o06o/gUIkQhDw9a9SLjuAyQUoLMfFCqkIeonUFzezds="
}

View File

@@ -0,0 +1,42 @@
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
# put this file in /etc/
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
ansi # and colors in log messages
# disable logging to stdout/journalctl and log to a file instead;
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
# full path will be something like /var/log/copyparty/2023-1130.txt
# (note: enable compression by adding .xz at the end)
q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
# ftp: 3921 # enable ftp server on port 3921
# p: 3939 # listen on another port
# df: 16 # stop accepting uploads if less than 16 GB free disk space
# ver # show copyparty version in the controlpanel
# grid # show thumbnails/grid-view by default
# theme: 2 # monokai
# name: datasaver # change the server-name that's displayed in the browser
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
# no-robots, force-js # make it harder for search engines to read your server
[accounts]
ed: wark # username: password
[/] # create a volume at "/" (the webroot), which will
/mnt # share the contents of the "/mnt" folder
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin

View File

@@ -1,28 +1,27 @@
# this will start `/usr/local/bin/copyparty-sfx.py`
# and share '/mnt' with anonymous read+write
# this will start `/usr/local/bin/copyparty-sfx.py` and
# read copyparty config from `/etc/copyparty.conf`, for example:
# https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.conf
#
# installation:
# wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py
# cp -pv copyparty.service /etc/systemd/system/
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
# firewall-cmd --permanent --add-port={80,443,3923}/tcp # --zone=libvirt
# useradd -r -s /sbin/nologin -d /var/lib/copyparty copyparty
# firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt
# firewall-cmd --reload
# cp -pv copyparty.service /etc/systemd/system/
# cp -pv copyparty.conf /etc/
# restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel
# systemctl daemon-reload && systemctl enable --now copyparty
#
# if it fails to start, first check this: systemctl status copyparty
# then try starting it while viewing logs: journalctl -fan 100
# then try starting it while viewing logs:
# journalctl -fan 100
# tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log)
#
# you may want to:
# change "User=cpp" and "/home/cpp/" to another user
# remove the nft lines to only listen on port 3923
# - change "User=copyparty" and "/var/lib/copyparty/" to another user
# - edit /etc/copyparty.conf to configure copyparty
# and in the ExecStart= line:
# change '/usr/bin/python3' to another interpreter
# change '/mnt::rw' to another location or permission-set
# add '-q' to disable logging on busy servers
# add '-i 127.0.0.1' to only allow local connections
# add '-e2dsa' to enable filesystem scanning + indexing
# add '-e2ts' to enable metadata indexing
# remove '--ansi' to disable colored logs
# - change '/usr/bin/python3' to another interpreter
#
# with `Type=notify`, copyparty will signal systemd when it is ready to
# accept connections; correctly delaying units depending on copyparty.
@@ -30,11 +29,9 @@
# python disabling line-buffering, so messages are out-of-order:
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
#
# keep ExecStartPre before ExecStart, at least on rhel8
########################################################################
########################################################################
[Unit]
Description=copyparty file server
@@ -44,23 +41,52 @@ Type=notify
SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x
ExecReload=/bin/kill -s USR1 $MAINPID
PermissionsStartOnly=true
# user to run as + where the TLS certificate is (if any)
User=cpp
Environment=XDG_CONFIG_HOME=/home/cpp/.config
## user to run as + where the TLS certificate is (if any)
##
User=copyparty
Group=copyparty
WorkingDirectory=/var/lib/copyparty
Environment=XDG_CONFIG_HOME=/var/lib/copyparty/.config
# OPTIONAL: setup forwarding from ports 80 and 443 to port 3923
ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true'
ExecStartPre=+nft add table ip nat
ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
ExecStartPre=+nft add rule ip nat prerouting tcp dport 80 redirect to :3923
ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
## OPTIONAL: allow copyparty to listen on low ports (like 80/443);
## you need to uncomment the "p: 80,443,3923" in the config too
## ------------------------------------------------------------
## a slightly safer alternative is to enable partyalone.service
## which does portforwarding with nftables instead, but an even
## better option is to use a reverse-proxy (nginx/caddy/...)
##
AmbientCapabilities=CAP_NET_BIND_SERVICE
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
## some quick hardening; TODO port more from the nixos package
##
MemoryMax=50%
MemorySwapMax=50%
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
RemoveIPC=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
# copyparty settings
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py --ansi -e2d -v /mnt::rw
## create a directory for logfiles;
## this defines $LOGS_DIRECTORY which is used in copyparty.conf
##
LogsDirectory=copyparty
## finally, start copyparty and give it the config file:
##
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -c /etc/copyparty.conf
# NOTE: if you installed copyparty from an OS package repo (nice)
# then you probably want something like this instead:
#ExecStart=/usr/bin/copyparty -c /etc/copyparty.conf
[Install]
WantedBy=multi-user.target

View File

@@ -1,5 +1,5 @@
# this will start `/usr/local/bin/copyparty-sfx.py`
# in a chroot, preventing accidental access elsewhere
# in a chroot, preventing accidental access elsewhere,
# and share '/mnt' with anonymous read+write
#
# installation:
@@ -7,9 +7,9 @@
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
#
# expose additional filesystem locations to copyparty
# by listing them between the last `1000` and `--`
# by listing them between the last `cpp` and `--`
#
# `1000 1000` = what user to run copyparty as
# `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000)
#
# you may want to:
# change '/mnt::rw' to another location or permission-set
@@ -32,7 +32,9 @@ ExecReload=/bin/kill -s USR1 $MAINPID
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# run copyparty
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail cpp cpp \
/mnt \
-- \
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
[Install]

View File

@@ -23,7 +23,7 @@ if not PY2:
unicode: Callable[[Any], str] = str
else:
sys.dont_write_bytecode = True
unicode = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable
unicode = unicode # type: ignore
WINDOWS: Any = (
[int(x) for x in platform.version().split(".")]

View File

@@ -19,11 +19,10 @@ import threading
import time
import traceback
import uuid
from textwrap import dedent
from .__init__ import ANYWIN, CORES, EXE, PY2, VT100, WINDOWS, E, EnvParams, unicode
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, re_vol, split_cfg_ln, upgrade_cfg_fmt
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .cfg import flagcats, onedash
from .svchub import SvcHub
from .util import (
@@ -37,6 +36,7 @@ from .util import (
UNPLICATIONS,
align_tab,
ansi_re,
dedent,
min_ex,
py_desc,
pybin,
@@ -143,9 +143,11 @@ def warn(msg: str) -> None:
lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))
def init_E(E: EnvParams) -> None:
def init_E(EE: EnvParams) -> None:
# __init__ runs 18 times when oxidized; do expensive stuff here
E = EE # pylint: disable=redefined-outer-name
def get_unixdir() -> str:
paths: list[tuple[Callable[..., Any], str]] = [
(os.environ.get, "XDG_CONFIG_HOME"),
@@ -246,7 +248,7 @@ def get_srvname() -> str:
return ret
def get_fk_salt(cert_path) -> str:
def get_fk_salt() -> str:
fp = os.path.join(E.cfg, "fk-salt.txt")
try:
with open(fp, "rb") as f:
@@ -320,6 +322,7 @@ def configure_ssl_ver(al: argparse.Namespace) -> None:
# oh man i love openssl
# check this out
# hold my beer
assert ssl
ptn = re.compile(r"^OP_NO_(TLS|SSL)v")
sslver = terse_sslver(al.ssl_ver).split(",")
flags = [k for k in ssl.__dict__ if ptn.match(k)]
@@ -353,6 +356,7 @@ def configure_ssl_ver(al: argparse.Namespace) -> None:
def configure_ssl_ciphers(al: argparse.Namespace) -> None:
assert ssl
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
if al.ssl_ver:
ctx.options &= ~al.ssl_flags_en
@@ -432,9 +436,9 @@ def disable_quickedit() -> None:
if PY2:
wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
k32.GetStdHandle.errcheck = ecb
k32.GetConsoleMode.errcheck = ecb
k32.SetConsoleMode.errcheck = ecb
k32.GetStdHandle.errcheck = ecb # type: ignore
k32.GetConsoleMode.errcheck = ecb # type: ignore
k32.SetConsoleMode.errcheck = ecb # type: ignore
k32.GetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.LPDWORD)
k32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD)
@@ -494,7 +498,9 @@ def get_sects():
"g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads
"h" (html): "get", but folders return their index.html
"." (dots): user can ask to show dotfiles in listings
"a" (admin): can see uploader IPs, config-reload
"A" ("all"): same as "rwmda." (read/write/move/delete/admin/dotfiles)
too many volflags to list here, see --help-flags
@@ -701,6 +707,7 @@ def get_sects():
\033[36mln\033[0m only prints symlinks leaving the volume mountpoint
\033[36mp\033[0m exits 1 if any such symlinks are found
\033[36mr\033[0m resumes startup after the listing
examples:
--ls '**' # list all files which are possible to read
--ls '**,*,ln' # check for dangerous symlinks
@@ -734,9 +741,12 @@ def get_sects():
"""
when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed
passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but copyparty will also hash and print any passwords that are non-hashed (password which do not start with '+') and then terminate afterwards
passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but
copyparty will also hash and print any passwords that are non-hashed
(password which do not start with '+') and then terminate afterwards
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a list of optional comma-separated arguments:
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a
list of optional comma-separated arguments:
\033[36m--ah-alg argon2\033[0m # which is the same as:
\033[36m--ah-alg argon2,3,256,4,19\033[0m
@@ -817,9 +827,9 @@ def add_general(ap, nc, srvname):
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m]")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see --help-urlform")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="window title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
ap2.add_argument("--version", action="store_true", help="show versions and exit")
@@ -830,7 +840,7 @@ def add_qr(ap, tty):
ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup")
ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup")
ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with PREFIX; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable")
ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
@@ -839,27 +849,28 @@ def add_qr(ap, tty):
def add_upload(ap):
ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed")
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than SEC seconds ago)")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without -e2d; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (very slow on windows)")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem) (volflag=hardlink)")
ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=neversymlink)")
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead (volflag=copydupes)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every SEC seconds; allows resuming incomplete uploads after a server crash")
ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440, help="forget unfinished uploads after MIN minutes; impossible to resume them after that (360=6h, 1440=24h)")
ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash")
ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
ap2.add_argument("--u2ts", metavar="TXT", type=u, default="c", help="how to timestamp uploaded files; [\033[32mc\033[0m]=client-last-modified, [\033[32mu\033[0m]=upload-time, [\033[32mfc\033[0m]=force-c, [\033[32mfu\033[0m]=force-u (volflag=u2ts)")
ap2.add_argument("--rand", action="store_true", help="force randomized filenames, --nrand chars long (volflag=rand)")
ap2.add_argument("--rand", action="store_true", help="force randomized filenames, \033[33m--nrand\033[0m chars long (volflag=rand)")
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
ap2.add_argument("--df", metavar="GiB", type=float, default=0, help="ensure GiB free disk space by rejecting upload requests")
ap2.add_argument("--df", metavar="GiB", type=float, default=0, help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16-32 for cross-atlantic (max=64)")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
@@ -868,11 +879,12 @@ def add_network(ap):
ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, default="::", help="ip to bind (comma-sep.), default: all IPv4 and IPv6")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)")
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 even if the NIC has routable IPs (breaks some mdns clients)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy")
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from (argument must be lowercase, but not the actual header)")
ap2.add_argument("--xff-src", metavar="IP", type=u, default="127., ::1", help="comma-separated list of trusted reverse-proxy IPs; only accept the real-ip header (--xff-hdr) if the incoming connection is from an IP starting with either of these. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using --xff-hdr=cf-connecting-ip (or similar)")
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here (eg. /foo/bar)")
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy")
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
ap2.add_argument("--xff-src", metavar="IP", type=u, default="127., ::1", help="comma-separated list of trusted reverse-proxy IPs; only accept the real-ip header (\033[33m--xff-hdr\033[0m) if the incoming connection is from an IP starting with either of these. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
ap2.add_argument("--ipa", metavar="PREFIX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPREFIX\033[0m; example: [\033[32m127., 10.89., 192.168.\033[0m]")
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
else:
@@ -882,7 +894,7 @@ def add_network(ap):
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0, help="debug: response delay, random duration 0..SEC")
ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m")
def add_tls(ap, cert_path):
@@ -901,7 +913,7 @@ def add_cert(ap, cert_path):
ap2 = ap.add_argument_group('TLS certificate generator options')
ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation")
ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate")
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each --crt-ns")
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m")
ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert")
ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
ap2.add_argument("--crt-nohn", action="store_true", help="do not add mDNS names / hostname into cert")
@@ -912,7 +924,14 @@ def add_cert(ap, cert_path):
ap2.add_argument("--crt-cnc", metavar="TXT", type=u, default="--crt-cn", help="override CA name")
ap2.add_argument("--crt-cns", metavar="TXT", type=u, default="--crt-cn cpp", help="override server-cert name")
ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72, help="backdate in hours")
ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: ecdsa-256 rsa-4096 rsa-2048")
ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: \033[32mecdsa-256 rsa-4096 rsa-2048\033[0m")
def add_auth(ap):
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
return
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
def add_zeroconf(ap):
@@ -920,21 +939,21 @@ def add_zeroconf(ap):
ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)")
ap2.add_argument("--z-on", metavar="NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes\n └─example: \033[32meth0, wlo1, virhost0, 192.168.123.0/24, fd00:fda::/96\033[0m")
ap2.add_argument("--z-off", metavar="NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--z-chk", metavar="SEC", type=int, default=10, help="check for network changes every SEC seconds (0=disable)")
ap2.add_argument("--z-chk", metavar="SEC", type=int, default=10, help="check for network changes every \033[33mSEC\033[0m seconds (0=disable)")
ap2.add_argument("-zv", action="store_true", help="verbose all zeroconf backends")
ap2.add_argument("--mc-hop", metavar="SEC", type=int, default=0, help="rejoin multicast groups every SEC seconds (workaround for some switches/routers which cause mDNS to suddenly stop working after some time); try [\033[32m300\033[0m] or [\033[32m180\033[0m]")
ap2.add_argument("--mc-hop", metavar="SEC", type=int, default=0, help="rejoin multicast groups every \033[33mSEC\033[0m seconds (workaround for some switches/routers which cause mDNS to suddenly stop working after some time); try [\033[32m300\033[0m] or [\033[32m180\033[0m]\n └─note: can be due to firewalls; make sure UDP port 5353 is open in both directions (on clients too)")
def add_zc_mdns(ap):
ap2 = ap.add_argument_group("Zeroconf-mDNS options; also see --help-zm")
ap2.add_argument("--zm", action="store_true", help="announce the enabled protocols over mDNS (multicast DNS-SD) -- compatible with KDE, gnome, macOS, ...")
ap2.add_argument("--zm-on", metavar="NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zm-off", metavar="NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zm-on", metavar="NETS", type=u, default="", help="enable mDNS ONLY on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zm-off", metavar="NETS", type=u, default="", help="disable mDNS on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zm4", action="store_true", help="IPv4 only -- try this if some clients can't connect")
ap2.add_argument("--zm6", action="store_true", help="IPv6 only")
ap2.add_argument("--zmv", action="store_true", help="verbose mdns")
ap2.add_argument("--zmvv", action="store_true", help="verboser mdns")
ap2.add_argument("--zms", metavar="dhf", type=u, default="", help="list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (\033[32mDdfs\033[0m if \033[33m--ftp\033[0m and \033[33m--smb\033[0m is set)")
ap2.add_argument("--zms", metavar="dhf", type=u, default="", help="list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (\033[32mDdfs\033[0m if \033[33m--ftp\033[0m and \033[33m--smb\033[0m is set, \033[32mDd\033[0m otherwise)")
ap2.add_argument("--zm-ld", metavar="PATH", type=u, default="", help="link a specific folder for webdav shares")
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
@@ -942,14 +961,14 @@ def add_zc_mdns(ap):
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
ap2.add_argument("--zm-spam", metavar="SEC", type=float, default=0, help="send unsolicited announce every SEC; useful if clients have IPs in a subnet which doesn't overlap with the server")
ap2.add_argument("--zm-spam", metavar="SEC", type=float, default=0, help="send unsolicited announce every \033[33mSEC\033[0m; useful if clients have IPs in a subnet which doesn't overlap with the server, or to avoid some firewall issues")
def add_zc_ssdp(ap):
ap2 = ap.add_argument_group("Zeroconf-SSDP options")
ap2.add_argument("--zs", action="store_true", help="announce the enabled protocols over SSDP -- compatible with Windows")
ap2.add_argument("--zs-on", metavar="NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zs-off", metavar="NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zs-on", metavar="NETS", type=u, default="", help="enable SSDP ONLY on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zs-off", metavar="NETS", type=u, default="", help="disable SSDP on the comma-separated list of subnets and/or interface names/indexes")
ap2.add_argument("--zsv", action="store_true", help="verbose SSDP")
ap2.add_argument("--zsl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] (goes directly to /priv/ with password hunter2) or [\033[32m?hc=priv&pw=hunter2\033[0m] (shows mounting options for /priv/ with password)")
ap2.add_argument("--zsid", metavar="UUID", type=u, default=zsid, help="USN (device identifier) to announce")
@@ -957,11 +976,12 @@ def add_zc_ssdp(ap):
def add_ftp(ap):
ap2 = ap.add_argument_group('FTP options')
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example \033[32m3921")
ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on PORT, for example \033[32m3990")
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921")
ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990")
ap2.add_argument("--ftpv", action="store_true", help="verbose")
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than SEC seconds ago)")
ap2.add_argument("--ftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]")
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections")
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
@@ -977,7 +997,7 @@ def add_webdav(ap):
def add_smb(ap):
ap2 = ap.add_argument_group('SMB/CIFS options')
ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless --smb-port is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is dangerous! Never expose to the internet!")
ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!")
ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)")
ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)")
ap2.add_argument("--smb-port", metavar="PORT", type=int, default=445, help="port to listen on -- if you change this value, you must NAT from TCP:445 to this port using iptables or similar")
@@ -991,22 +1011,22 @@ def add_smb(ap):
def add_handlers(ap):
ap2 = ap.add_argument_group('handlers (see --help-handlers)')
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="handle 404s by executing PY file")
ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing PY file")
ap2.add_argument("--hot-handlers", action="store_true", help="reload handlers on each request -- expensive but convenient when hacking on stuff")
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="handle 404s by executing \033[33mPY\033[0m file")
ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing \033[33mPY\033[0m file")
ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff")
def add_hooks(ap):
ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute CMD after a file upload finishes")
ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute CMD after all uploads finish and volume is idle")
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute CMD before a file move/rename")
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute CMD after a file move/rename")
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute CMD if someone gets banned (pw/404/403/url)")
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file upload starts")
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file upload finishes")
ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after all uploads finish and volume is idle")
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file move/rename")
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file move/rename")
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m on message")
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)")
def add_stats(ap):
@@ -1028,17 +1048,17 @@ def add_yolo(ap):
def add_optouts(ap):
ap2 = ap.add_argument_group('opt-outs')
ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection which will deadlock copyparty)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)")
ap2.add_argument("--no-dav", action="store_true", help="disable webdav support")
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show --name in <title>")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (as specified by the 'lifetime' volflag)")
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
def add_safety(ap):
@@ -1046,36 +1066,36 @@ def add_safety(ap):
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m; example [\033[32m**,*,ln,p,r\033[0m]")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore --no-robots")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after H hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (422 is server fuzzing, invalid POSTs and so)")
ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; applies only to access g/G/h (decent replacement for --ban-404 if that can't be used)")
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; applies only to permissions g/G/h (decent replacement for \033[33m--ban-404\033[0m if that can't be used)")
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for MIN minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for B minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like described in --acao)")
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
def add_salt(ap, fk_salt, ah_salt):
ap2 = ap.add_argument_group('salting options')
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: argon2 scrypt sha2 none (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if --ah-alg is none (default)")
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
ap2.add_argument("--ah-cli", action="store_true", help="interactive shell which hashes passwords without ever storing or displaying the original passwords")
ap2.add_argument("--ah-cli", action="store_true", help="launch an interactive shell which hashes passwords without ever storing or displaying the original passwords")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
@@ -1084,22 +1104,23 @@ def add_shutdown(ap):
ap2 = ap.add_argument_group('shutdown options')
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after WHEN has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing")
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing")
def add_logging(ap):
ap2 = ap.add_argument_group('logging options')
ap2.add_argument("-q", action="store_true", help="quiet")
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=1, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
def add_admin(ap):
@@ -1117,16 +1138,16 @@ def add_thumbnail(ap):
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (volflag=nocrop)")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (client can override in UI) (volflag=nocrop)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs")
ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than SEC seconds")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling -e2d will make these case-insensitive, and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
@@ -1141,14 +1162,14 @@ def add_transcoding(ap):
ap2 = ap.add_argument_group('transcoding options')
ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)")
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after SEC seconds")
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
def add_db_general(ap, hcores):
ap2 = ap.add_argument_group('general db options')
ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplication")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets -e2d")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets -e2ds")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m")
ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db")
ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
@@ -1157,13 +1178,13 @@ def add_db_general(ap, hcores):
ap2.add_argument("--no-idx", metavar="PTN", type=u, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
ap2.add_argument("--re-dhash", action="store_true", help="rebuild the cache if it gets out of sync (for example crash on startup during metadata scanning)")
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice (volflag=noforget)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see --help-dbd (volflag=dbd)")
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (volflag=xlink)")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off (volflag=scan)")
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10, help="defer any scheduled volume reindexing until SEC seconds after last db write (uploads, renames, ...)")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than SEC seconds")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
ap2.add_argument("--dotsrch", action="store_true", help="show dotfiles in search results (volflags: dotsrch | nodotsrch)")
@@ -1171,25 +1192,25 @@ def add_db_general(ap, hcores):
def add_db_metadata(ap):
ap2 = ap.add_argument_group('metadata db options')
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...")
ap2.add_argument("-e2ts", action="store_true", help="scan existing files on startup; sets -e2t")
ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets -e2ts")
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will catch more tags")
ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m")
ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m")
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will detect more tags")
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader; is probably safer")
ap2.add_argument("--mtag-to", metavar="SEC", type=int, default=60, help="timeout for ffprobe tag-scan")
ap2.add_argument("--mtag-to", metavar="SEC", type=int, default=60, help="timeout for FFprobe tag-scan")
ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning")
ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such")
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers")
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE)
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as -mte", default=DEF_MTH)
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH)
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file")
def add_txt(ap):
ap2 = ap.add_argument_group('textfile options')
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="textfile editor checks for serverside changes every SEC seconds")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see --help-exp (volflag=exp)")
ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)")
ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)")
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
@@ -1197,11 +1218,11 @@ def add_txt(ap):
def add_ui(ap, retry):
ap2 = ap.add_argument_group('ui options')
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: eng nor")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching REGEX in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
@@ -1212,8 +1233,8 @@ def add_ui(ap, retry):
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with -np")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with -nb)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default="https://github.com/9001/copyparty", help="powered-by link; disable with \033[33m-np\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox")
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to ALLOW for prologue/epilogue docs (volflag=lg_sbf)")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README.md documents (volflags: no_sb_md | sb_md)")
@@ -1224,18 +1245,18 @@ def add_debug(ap):
ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile; instead using a traditional file read loop")
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir; instead using listdir + stat on each file")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing before starting the httpd")
ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead")
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches REGEX; [\033[32m.\033[0m] (a single dot) = all files")
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to --bf-nc and --bf-dir")
ap2.add_argument("--bf-nc", metavar="NUM", type=int, default=200, help="bak-flips: stop if there's more than NUM files at --kf-dir already; default: 6.3 GiB max (200*32M)")
ap2.add_argument("--bf-dir", metavar="PATH", type=u, default="bf", help="bak-flips: store corrupted chunks at PATH; default: folder named 'bf' wherever copyparty was started")
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every \033[33mSEC\033[0m")
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files")
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")
ap2.add_argument("--bf-nc", metavar="NUM", type=int, default=200, help="bak-flips: stop if there's more than \033[33mNUM\033[0m files at \033[33m--kf-dir\033[0m already; default: 6.3 GiB max (200*32M)")
ap2.add_argument("--bf-dir", metavar="PATH", type=u, default="bf", help="bak-flips: store corrupted chunks at \033[33mPATH\033[0m; default: folder named 'bf' wherever copyparty was started")
# fmt: on
@@ -1252,7 +1273,7 @@ def run_argparse(
cert_path = os.path.join(E.cfg, "cert.pem")
fk_salt = get_fk_salt(cert_path)
fk_salt = get_fk_salt()
ah_salt = get_ah_salt()
# alpine peaks at 5 threads for some reason,
@@ -1268,6 +1289,7 @@ def run_argparse(
add_network(ap)
add_tls(ap, cert_path)
add_cert(ap, cert_path)
add_auth(ap)
add_qr(ap, tty)
add_zeroconf(ap)
add_zc_mdns(ap)
@@ -1369,7 +1391,10 @@ def main(argv: Optional[list[str]] = None) -> None:
supp = args_from_cfg(v)
argv.extend(supp)
deprecated: list[tuple[str, str]] = [("--salt", "--warksalt")]
deprecated: list[tuple[str, str]] = [
("--salt", "--warksalt"),
("--hdr-au-usr", "--idp-h-usr"),
]
for dk, nk in deprecated:
idx = -1
ov = ""
@@ -1444,40 +1469,6 @@ def main(argv: Optional[list[str]] = None) -> None:
if al.ansi:
al.wintitle = ""
nstrs: list[str] = []
anymod = False
for ostr in al.v or []:
m = re_vol.match(ostr)
if not m:
# not our problem
nstrs.append(ostr)
continue
src, dst, perms = m.groups()
na = [src, dst]
mod = False
for opt in perms.split(":"):
if re.match("c[^,]", opt):
mod = True
na.append("c," + opt[1:])
elif re.sub("^[rwmdgGha]*", "", opt) and "," not in opt:
mod = True
perm = opt[0]
na.append(perm + "," + opt[1:])
else:
na.append(opt)
nstr = ":".join(na)
nstrs.append(nstr if mod else ostr)
if mod:
msg = "\033[1;31mWARNING:\033[0;1m\n -v {} \033[0;33mwas replaced with\033[0;1m\n -v {} \n\033[0m"
lprint(msg.format(ostr, nstr))
anymod = True
if anymod:
al.v = nstrs
time.sleep(2)
# propagate implications
for k1, k2 in IMPLICATIONS:
if getattr(al, k1):

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 9, 19)
VERSION = (1, 9, 28)
CODENAME = "prometheable"
BUILD_DT = (2023, 11, 19)
BUILD_DT = (2023, 12, 31)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -72,6 +72,7 @@ class AXS(object):
upget: Optional[Union[list[str], set[str]]] = None,
uhtml: Optional[Union[list[str], set[str]]] = None,
uadmin: Optional[Union[list[str], set[str]]] = None,
udot: Optional[Union[list[str], set[str]]] = None,
) -> None:
self.uread: set[str] = set(uread or [])
self.uwrite: set[str] = set(uwrite or [])
@@ -81,9 +82,10 @@ class AXS(object):
self.upget: set[str] = set(upget or [])
self.uhtml: set[str] = set(uhtml or [])
self.uadmin: set[str] = set(uadmin or [])
self.udot: set[str] = set(udot or [])
def __repr__(self) -> str:
ks = "uread uwrite umove udel uget upget uhtml uadmin".split()
ks = "uread uwrite umove udel uget upget uhtml uadmin udot".split()
return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
@@ -336,6 +338,8 @@ class VFS(object):
self.apget: dict[str, list[str]] = {}
self.ahtml: dict[str, list[str]] = {}
self.aadmin: dict[str, list[str]] = {}
self.adot: dict[str, list[str]] = {}
self.all_vols: dict[str, VFS] = {}
if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
@@ -414,7 +418,7 @@ class VFS(object):
hist = flags.get("hist")
if hist and hist != "-":
zs = "{}/{}".format(hist.rstrip("/"), name)
flags["hist"] = os.path.expanduser(zs) if zs.startswith("~") else zs
flags["hist"] = os.path.expandvars(os.path.expanduser(zs))
return flags
@@ -445,8 +449,8 @@ class VFS(object):
def can_access(
self, vpath: str, uname: str
) -> tuple[bool, bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget,Admin"""
) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget,Admin,Dot"""
if vpath:
vn, _ = self._find(undot(vpath))
else:
@@ -454,13 +458,14 @@ class VFS(object):
c = vn.axs
return (
uname in c.uread or "*" in c.uread,
uname in c.uwrite or "*" in c.uwrite,
uname in c.umove or "*" in c.umove,
uname in c.udel or "*" in c.udel,
uname in c.uget or "*" in c.uget,
uname in c.upget or "*" in c.upget,
uname in c.uadmin or "*" in c.uadmin,
uname in c.uread,
uname in c.uwrite,
uname in c.umove,
uname in c.udel,
uname in c.uget,
uname in c.upget,
uname in c.uadmin,
uname in c.udot,
)
# skip uhtml because it's rarely needed
@@ -492,7 +497,7 @@ class VFS(object):
(will_del, c.udel, "delete"),
(will_get, c.uget, "get"),
]:
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
if req and uname not in d and uname != LEELOO_DALLAS:
if vpath != cvpath and vpath != "." and self.log:
ap = vn.canonical(rem)
t = "{} has no {} in [{}] => [{}] => [{}]"
@@ -553,7 +558,7 @@ class VFS(object):
for pset in permsets:
ok = True
for req, lst in zip(pset, axs):
if req and uname not in lst and "*" not in lst:
if req and uname not in lst:
ok = False
if ok:
break
@@ -577,7 +582,7 @@ class VFS(object):
seen: list[str],
uname: str,
permsets: list[list[bool]],
dots: bool,
wantdots: bool,
scandir: bool,
lstat: bool,
subvols: bool = True,
@@ -621,6 +626,10 @@ class VFS(object):
rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
dots_ok = wantdots and uname in dbv.axs.udot
if not dots_ok:
vfs_ls = [x for x in vfs_ls if "/." not in "/" + x[0]]
seen = seen[:] + [fsroot]
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
@@ -633,13 +642,13 @@ class VFS(object):
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
for rdir, _ in rdirs:
if not dots and rdir.startswith("."):
if not dots_ok and rdir.startswith("."):
continue
wrel = (rel + "/" + rdir).lstrip("/")
wrem = (rem + "/" + rdir).lstrip("/")
for x in self.walk(
wrel, wrem, seen, uname, permsets, dots, scandir, lstat, subvols
wrel, wrem, seen, uname, permsets, wantdots, scandir, lstat, subvols
):
yield x
@@ -647,11 +656,13 @@ class VFS(object):
return
for n, vfs in sorted(vfs_virt.items()):
if not dots and n.startswith("."):
if not dots_ok and n.startswith("."):
continue
wrel = (rel + "/" + n).lstrip("/")
for x in vfs.walk(wrel, "", seen, uname, permsets, dots, scandir, lstat):
for x in vfs.walk(
wrel, "", seen, uname, permsets, wantdots, scandir, lstat
):
yield x
def zipgen(
@@ -660,7 +671,6 @@ class VFS(object):
vrem: str,
flt: set[str],
uname: str,
dots: bool,
dirs: bool,
scandir: bool,
wrap: bool = True,
@@ -670,7 +680,7 @@ class VFS(object):
# if single folder: the folder itself is the top-level item
folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top")
g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False)
g = self.walk(folder, vrem, [], uname, [[True, False]], True, scandir, False)
for _, _, vpath, apath, files, rd, vd in g:
if flt:
files = [x for x in files if x[0] in flt]
@@ -689,18 +699,6 @@ class VFS(object):
apaths = [os.path.join(apath, n) for n in fnames]
ret = list(zip(vpaths, apaths, files))
if not dots:
# dotfile filtering based on vpath (intended visibility)
ret = [x for x in ret if "/." not in "/" + x[0]]
zel = [ze for ze in rd if ze[0].startswith(".")]
for ze in zel:
rd.remove(ze)
zsl = [zs for zs in vd.keys() if zs.startswith(".")]
for zs in zsl:
del vd[zs]
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in ret]:
yield f
@@ -947,9 +945,7 @@ class AuthSrv(object):
if vp is not None and ap is None:
ap = ln
if ap.startswith("~"):
ap = os.path.expanduser(ap)
ap = os.path.expandvars(os.path.expanduser(ap))
ap = absreal(ap)
self._l(ln, 2, "bound to filesystem-path [{}]".format(ap))
self._map_volume(ap, vp, mount, daxs, mflags)
@@ -960,16 +956,17 @@ class AuthSrv(object):
try:
self._l(ln, 5, "volume access config:")
sk, sv = ln.split(":")
if re.sub("[rwmdgGha]", "", sk) or not sk:
if re.sub("[rwmdgGhaA.]", "", sk) or not sk:
err = "invalid accs permissions list; "
raise Exception(err)
if " " in re.sub(", *", "", sv).strip():
err = "list of users is not comma-separated; "
raise Exception(err)
assert vp is not None
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
continue
except:
err += "accs entries must be 'rwmdgGha: user1, user2, ...'"
err += "accs entries must be 'rwmdgGhaA.: user1, user2, ...'"
raise Exception(err + SBADCFG)
if cat == catf:
@@ -988,9 +985,11 @@ class AuthSrv(object):
fstr += "," + sk
else:
fstr += ",{}={}".format(sk, sv)
assert vp is not None
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
fstr = ""
if fstr:
assert vp is not None
self._read_vol_str("c", fstr[1:], daxs[vp], mflags[vp])
continue
except:
@@ -1005,8 +1004,9 @@ class AuthSrv(object):
def _read_vol_str(
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
) -> None:
if lvl.strip("crwmdgGha"):
raise Exception("invalid volflag: {},{}".format(lvl, uname))
if lvl.strip("crwmdgGhaA."):
t = "%s,%s" % (lvl, uname) if uname else lvl
raise Exception("invalid config value (volume or volflag): %s" % (t,))
if lvl == "c":
cval: Union[bool, str] = True
@@ -1028,19 +1028,31 @@ class AuthSrv(object):
if uname == "":
uname = "*"
junkset = set()
for un in uname.replace(",", " ").strip().split():
for alias, mapping in [
("h", "gh"),
("G", "gG"),
("A", "rwmda.A"),
]:
expanded = ""
for ch in mapping:
if ch not in lvl:
expanded += ch
lvl = lvl.replace(alias, expanded + alias)
for ch, al in [
("r", axs.uread),
("w", axs.uwrite),
("m", axs.umove),
("d", axs.udel),
(".", axs.udot),
("a", axs.uadmin),
("h", axs.uhtml),
("h", axs.uget),
("A", junkset),
("g", axs.uget),
("G", axs.uget),
("G", axs.upget),
]: # b bb bbb
("h", axs.uhtml),
]:
if ch in lvl:
if un == "*":
t = "└─add permission [{0}] for [everyone] -- {2}"
@@ -1112,7 +1124,7 @@ class AuthSrv(object):
if self.args.v:
# list of src:dst:permset:permset:...
# permset is <rwmdgGha>[,username][,username] or <c>,<flag>[=args]
# permset is <rwmdgGhaA.>[,username][,username] or <c>,<flag>[=args]
for v_str in self.args.v:
m = re_vol.match(v_str)
if not m:
@@ -1186,12 +1198,13 @@ class AuthSrv(object):
vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
continue
assert vfs # type: ignore
zv = vfs.add(mount[dst], dst)
zv.axs = daxs[dst]
zv.flags = mflags[dst]
zv.dbv = None
assert vfs
assert vfs # type: ignore
vfs.all_vols = {}
vfs.all_aps = []
vfs.all_vps = []
@@ -1201,14 +1214,21 @@ class AuthSrv(object):
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs
for perm in "read write move del get pget html admin".split():
for perm in "read write move del get pget html admin dot".split():
axs_key = "u" + perm
unames = ["*"] + list(acct.keys())
for vp, vol in vfs.all_vols.items():
zx = getattr(vol.axs, axs_key)
if "*" in zx:
for usr in unames:
zx.add(usr)
# aread,... = dict[uname, list[volnames] or []]
umap: dict[str, list[str]] = {x: [] for x in unames}
for usr in unames:
for vp, vol in vfs.all_vols.items():
zx = getattr(vol.axs, axs_key)
if usr in zx or "*" in zx:
if usr in zx:
umap[usr].append(vp)
umap[usr].sort()
setattr(vfs, "a" + perm, umap)
@@ -1225,6 +1245,7 @@ class AuthSrv(object):
axs.upget,
axs.uhtml,
axs.uadmin,
axs.udot,
]:
for usr in d:
all_users[usr] = 1
@@ -1259,9 +1280,7 @@ class AuthSrv(object):
if vflag == "-":
pass
elif vflag:
if vflag.startswith("~"):
vflag = os.path.expanduser(vflag)
vflag = os.path.expandvars(os.path.expanduser(vflag))
vol.histpath = uncyg(vflag) if WINDOWS else vflag
elif self.args.hist:
for nch in range(len(hid)):
@@ -1635,6 +1654,11 @@ class AuthSrv(object):
vol.flags.pop(k[1:], None)
vol.flags.pop(k)
for vol in vfs.all_vols.values():
if vol.flags.get("dots"):
for name in vol.axs.uread:
vol.axs.udot.add(name)
if errors:
sys.exit(1)
@@ -1653,12 +1677,14 @@ class AuthSrv(object):
[" write", "uwrite"],
[" move", "umove"],
["delete", "udel"],
[" dots", "udot"],
[" get", "uget"],
[" upget", "upget"],
[" upGet", "upget"],
[" html", "uhtml"],
["uadmin", "uadmin"],
]:
u = list(sorted(getattr(zv.axs, attr)))
u = ["*"] if "*" in u else u
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
u = u if u else "\033[36m--none--\033[0m"
t += "\n| {}: {}".format(txt, u)
@@ -1815,7 +1841,7 @@ class AuthSrv(object):
raise Exception("volume not found: " + zs)
self.log(str({"users": users, "vols": vols, "flags": flags}))
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({}) uadmin({})"
t = "/{}: read({}) write({}) move({}) del({}) dots({}) get({}) upGet({}) uadmin({})"
for k, zv in self.vfs.all_vols.items():
vc = zv.axs
vs = [
@@ -1824,6 +1850,7 @@ class AuthSrv(object):
vc.uwrite,
vc.umove,
vc.udel,
vc.udot,
vc.uget,
vc.upget,
vc.uhtml,
@@ -1966,6 +1993,7 @@ class AuthSrv(object):
"w": "uwrite",
"m": "umove",
"d": "udel",
".": "udot",
"g": "uget",
"G": "upget",
"h": "uhtml",
@@ -2172,7 +2200,7 @@ def upgrade_cfg_fmt(
else:
sn = sn.replace(",", ", ")
ret.append(" " + sn)
elif sn[:1] in "rwmdgGha":
elif sn[:1] in "rwmdgGhaA.":
if cat != catx:
cat = catx
ret.append(cat)

View File

@@ -43,6 +43,10 @@ def open(p: str, *a, **ka) -> int:
return os.open(fsenc(p), *a, **ka)
def readlink(p: str) -> str:
return fsdec(os.readlink(fsenc(p)))
def rename(src: str, dst: str) -> None:
return os.rename(fsenc(src), fsenc(dst))

View File

@@ -46,8 +46,8 @@ class BrokerMp(object):
self.num_workers = self.args.j or CORES
self.log("broker", "booting {} subprocesses".format(self.num_workers))
for n in range(1, self.num_workers + 1):
q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1)
q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64)
q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1) # type: ignore
q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64) # type: ignore
proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n))
Daemon(self.collector, "mp-sink-{}".format(n), (proc,))

View File

@@ -9,6 +9,9 @@ onedash = set(zs.split())
def vf_bmap() -> dict[str, str]:
"""argv-to-volflag: simple bools"""
ret = {
"dav_auth": "davauth",
"dav_rt": "davrt",
"ed": "dots",
"never_symlink": "neversymlink",
"no_dedup": "copydupes",
"no_dupe": "nodupe",
@@ -18,8 +21,6 @@ def vf_bmap() -> dict[str, str]:
"no_vthumb": "dvthumb",
"no_athumb": "dathumb",
"th_no_crop": "nocrop",
"dav_auth": "davauth",
"dav_rt": "davrt",
}
for k in (
"dotsrch",
@@ -98,10 +99,12 @@ permdescs = {
"w": 'write; upload files; need "r" to see the uploads',
"m": 'move; move files and folders; need "w" at destination',
"d": "delete; permanently delete files and folders",
".": "dots; user can ask to show dotfiles in listings",
"g": "get; download files, but cannot see folder contents",
"G": 'upget; same as "g" but can see filekeys of their own uploads',
"h": 'html; same as "g" but folders return their index.html',
"a": "admin; can see uploader IPs, config-reload",
"A": "all; same as 'rwmda.' (read/write/move/delete/dotfiles)",
}
@@ -202,6 +205,7 @@ flagcats = {
"nohtml": "return html and markdown as text/html",
},
"others": {
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
"davauth": "ask webdav clients to login for all folders",

View File

@@ -15,7 +15,7 @@ from pyftpdlib.handlers import FTPHandler
from pyftpdlib.ioloop import IOLoop
from pyftpdlib.servers import FTPServer
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, E
from .__init__ import PY2, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import (
@@ -73,6 +73,7 @@ class FtpAuth(DummyAuthorizer):
asrv = self.hub.asrv
uname = "*"
if username != "anonymous":
uname = ""
for zs in (password, username):
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs:
@@ -88,8 +89,8 @@ class FtpAuth(DummyAuthorizer):
bans[ip] = bonk
try:
# only possible if multiprocessing disabled
self.hub.broker.httpsrv.bans[ip] = bonk
self.hub.broker.httpsrv.nban += 1
self.hub.broker.httpsrv.bans[ip] = bonk # type: ignore
self.hub.broker.httpsrv.nban += 1 # type: ignore
except:
pass
@@ -132,7 +133,7 @@ class FtpFs(AbstractedFS):
self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False
self.can_admin = False
self.can_admin = self.can_dot = False
self.listdirinfo = self.listdir
self.chdir(".")
@@ -167,7 +168,7 @@ class FtpFs(AbstractedFS):
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _, _ = avfs.can_access("", self.h.uname)
cr, cw, cm, cd, _, _, _, _ = avfs.can_access("", self.h.uname)
if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1)
@@ -243,6 +244,7 @@ class FtpFs(AbstractedFS):
self.can_get,
self.can_upget,
self.can_admin,
self.can_dot,
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:
@@ -265,7 +267,7 @@ class FtpFs(AbstractedFS):
vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys())
if not self.args.ed:
if not self.can_dot:
vfs_ls = exclude_dotfiles(vfs_ls)
vfs_ls.sort()
@@ -404,7 +406,16 @@ class FtpHandler(FTPHandler):
super(FtpHandler, self).__init__(conn, server, ioloop)
cip = self.remote_ip
self.cli_ip = cip[7:] if cip.startswith("::ffff:") else cip
if cip.startswith("::ffff:"):
cip = cip[7:]
if self.args.ftp_ipa_re and not self.args.ftp_ipa_re.match(cip):
logging.warning("client rejected (--ftp-ipa): %s", cip)
self.connected = False
conn.close()
return
self.cli_ip = cip
# abspath->vpath mapping to resolve log_transfer paths
self.vfs_map: dict[str, str] = {}

View File

@@ -81,7 +81,7 @@ from .util import (
sendfile_py,
undot,
unescape_cookie,
unquote,
unquote, # type: ignore
unquotep,
vjoin,
vol_san,
@@ -135,7 +135,7 @@ class HttpCli(object):
self.headers: dict[str, str] = {}
self.mode = " "
self.req = " "
self.http_ver = " "
self.http_ver = ""
self.hint = ""
self.host = " "
self.ua = " "
@@ -154,10 +154,6 @@ class HttpCli(object):
self.pw = " "
self.rvol = [" "]
self.wvol = [" "]
self.mvol = [" "]
self.dvol = [" "]
self.gvol = [" "]
self.upvol = [" "]
self.avol = [" "]
self.do_log = True
self.can_read = False
@@ -167,6 +163,7 @@ class HttpCli(object):
self.can_get = False
self.can_upget = False
self.can_admin = False
self.can_dot = False
self.out_headerlist: list[tuple[str, str]] = []
self.out_headers: dict[str, str] = {}
self.html_head = " "
@@ -236,6 +233,11 @@ class HttpCli(object):
if self.is_banned():
return False
if self.args.ipa_re and not self.args.ipa_re.match(self.conn.addr[0]):
self.log("client rejected (--ipa)", 3)
self.terse_reply(b"", 500)
return False
try:
self.s.settimeout(2)
headerlines = read_header(self.sr, self.args.s_thead, self.args.s_thead)
@@ -368,22 +370,33 @@ class HttpCli(object):
self.trailing_slash = vpath.endswith("/")
vpath = undot(vpath)
zs = unquotep(arglist)
m = self.conn.hsrv.ptn_cc.search(zs)
if m:
hit = zs[m.span()[0] :]
t = "malicious user; Cc in query [{}] => [{!r}]"
self.log(t.format(self.req, hit), 1)
return False
ptn = self.conn.hsrv.ptn_cc
for k in arglist.split("&"):
if "=" in k:
k, zs = k.split("=", 1)
# x-www-form-urlencoded (url query part) uses
# either + or %20 for 0x20 so handle both
uparam[k.lower()] = unquotep(zs.strip().replace("+", " "))
sv = unquotep(zs.strip().replace("+", " "))
else:
uparam[k.lower()] = ""
sv = ""
k = k.lower()
uparam[k] = sv
if k in ("doc", "move", "tree"):
continue
zs = "%s=%s" % (k, sv)
m = ptn.search(zs)
if not m:
continue
hit = zs[m.span()[0] :]
t = "malicious user; Cc in query [{}] => [{!r}]"
self.log(t.format(self.req, hit), 1)
self.cbonk(self.conn.hsrv.gmal, self.req, "cc_q", "Cc in query")
self.terse_reply(b"", 500)
return False
if self.is_vproxied:
if vpath.startswith(self.args.R):
@@ -422,7 +435,7 @@ class HttpCli(object):
if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"):
self.log("invalid relpath [{}]".format(self.vpath))
self.cbonk(self.conn.hsrv.g422, self.vpath, "bad_vp", "invalid relpaths")
self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths")
return self.tx_404() and self.keepalive
zso = self.headers.get("authorization")
@@ -439,14 +452,18 @@ class HttpCli(object):
except:
pass
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
self.uname = self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*"
if self.args.idp_h_usr:
self.pw = ""
self.uname = self.headers.get(self.args.idp_h_usr) or "*"
if self.uname not in self.asrv.vfs.aread:
self.log("unknown username: [%s]" % (self.uname), 1)
self.uname = "*"
else:
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
self.uname = self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*"
self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname]
self.mvol = self.asrv.vfs.amove[self.uname]
self.dvol = self.asrv.vfs.adel[self.uname]
self.gvol = self.asrv.vfs.aget[self.uname]
self.upvol = self.asrv.vfs.apget[self.uname]
self.avol = self.asrv.vfs.aadmin[self.uname]
if self.pw and (
@@ -479,8 +496,9 @@ class HttpCli(object):
self.can_get,
self.can_upget,
self.can_admin,
self.can_dot,
) = (
avn.can_access("", self.uname) if avn else [False] * 7
avn.can_access("", self.uname) if avn else [False] * 8
)
self.avn = avn
self.vn = vn
@@ -530,6 +548,7 @@ class HttpCli(object):
try:
if pex.code == 999:
self.terse_reply(b"", 500)
return False
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
@@ -614,8 +633,7 @@ class HttpCli(object):
return False
self.log("banned for {:.0f} sec".format(rt), 6)
zb = b"HTTP/1.0 403 Forbidden\r\n\r\nthank you for playing"
self.s.sendall(zb)
self.terse_reply(b"thank you for playing", 403)
return True
def permit_caching(self) -> None:
@@ -669,6 +687,7 @@ class HttpCli(object):
hit = zs[m.span()[0] :]
t = "malicious user; Cc in out-hdr {!r} => [{!r}]"
self.log(t.format(zs, hit), 1)
self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
raise Pebkac(999)
try:
@@ -745,6 +764,19 @@ class HttpCli(object):
self.log(body.rstrip())
self.reply(body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
def terse_reply(self, body: bytes, status: int = 200) -> None:
self.keepalive = False
lines = [
"%s %s %s" % (self.http_ver or "HTTP/1.1", status, HTTPCODE[status]),
"Connection: Close",
]
if body:
lines.append("Content-Length: " + unicode(len(body)))
self.s.sendall("\r\n".join(lines).encode("utf-8") + b"\r\n\r\n" + body)
def urlq(self, add: dict[str, str], rm: list[str]) -> str:
"""
generates url query based on uparam (b, pw, all others)
@@ -829,16 +861,16 @@ class HttpCli(object):
self.host.lower().split(":")[0],
)
]
if re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins:
if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins:
good_origin = True
bad_hdrs = ("",)
else:
good_origin = False
bad_hdrs = ("", "pw")
# '*' blocks all credentials (cookies, http-auth);
# '*' blocks auth through cookies / WWW-Authenticate;
# exact-match for Origin is necessary to unlock those,
# however yolo-requests (?pw=) are always allowed
# but the ?pw= param and PW: header are always allowed
acah = ih.get("access-control-request-headers", "")
acao = (origin if good_origin else None) or (
"*" if "*" in good_origins else None
@@ -888,7 +920,11 @@ class HttpCli(object):
return self.tx_ico(self.vpath.split("/")[-1], exact=True)
if self.vpath.startswith(".cpr/ssdp"):
return self.conn.hsrv.ssdp.reply(self)
if self.conn.hsrv.ssdp:
return self.conn.hsrv.ssdp.reply(self)
else:
self.reply(b"ssdp is disabled in server config", 404)
return False
if self.vpath.startswith(".cpr/dd/") and self.args.mpmc:
if self.args.mpmc == ".":
@@ -910,6 +946,7 @@ class HttpCli(object):
if not static_path.startswith(path_base):
t = "malicious user; attempted path traversal [{}] => [{}]"
self.log(t.format(self.vpath, static_path), 1)
self.cbonk(self.conn.hsrv.gmal, self.req, "trav", "path traversal")
self.tx_404()
return False
@@ -1088,7 +1125,6 @@ class HttpCli(object):
rem,
set(),
self.uname,
self.args.ed,
True,
not self.args.no_scandir,
wrap=False,
@@ -1102,7 +1138,7 @@ class HttpCli(object):
[[True, False]],
lstat="davrt" not in vn.flags,
)
if not self.args.ed:
if not self.can_dot:
names = set(exclude_dotfiles([x[0] for x in vfs_ls]))
vfs_ls = [x for x in vfs_ls if x[0] in names]
@@ -1867,7 +1903,7 @@ class HttpCli(object):
items = [unquotep(x) for x in items if items]
self.parser.drop()
return self.tx_zip(k, v, "", vn, rem, items, self.args.ed)
return self.tx_zip(k, v, "", vn, rem, items)
def handle_post_json(self) -> bool:
try:
@@ -1953,10 +1989,10 @@ class HttpCli(object):
def handle_search(self, body: dict[str, Any]) -> bool:
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
raise Pebkac(500, "sqlite3 is not available on the server; cannot search")
raise Pebkac(500, "server busy, or sqlite3 not available; cannot search")
vols = []
seen = {}
vols: list[VFS] = []
seen: dict[VFS, bool] = {}
for vtop in self.rvol:
vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False)
vfs = vfs.dbv or vfs
@@ -1964,7 +2000,7 @@ class HttpCli(object):
continue
seen[vfs] = True
vols.append((vfs.vpath, vfs.realpath, vfs.flags))
vols.append(vfs)
t0 = time.time()
if idx.p_end:
@@ -1979,7 +2015,7 @@ class HttpCli(object):
vbody = copy.deepcopy(body)
vbody["hash"] = len(vbody["hash"])
self.log("qj: " + repr(vbody))
hits = idx.fsearch(vols, body)
hits = idx.fsearch(self.uname, vols, body)
msg: Any = repr(hits)
taglist: list[str] = []
trunc = False
@@ -1988,7 +2024,7 @@ class HttpCli(object):
q = body["q"]
n = body.get("n", self.args.srch_hits)
self.log("qj: {} |{}|".format(q, n))
hits, taglist, trunc = idx.search(vols, q, n)
hits, taglist, trunc = idx.search(self.uname, vols, q, n)
msg = len(hits)
idx.p_end = time.time()
@@ -2161,17 +2197,17 @@ class HttpCli(object):
msg = "naw dude"
pwd = "x" # nosec
dur = None
dur = 0
if pwd == "x":
# reset both plaintext and tls
# (only affects active tls cookies when tls)
for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
ck = gencookie(k, pwd, self.args.R, False, dur)
ck = gencookie(k, pwd, self.args.R, False)
self.out_headerlist.append(("Set-Cookie", ck))
else:
k = "cppws" if self.is_https else "cppwd"
ck = gencookie(k, pwd, self.args.R, self.is_https, dur)
ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly")
self.out_headerlist.append(("Set-Cookie", ck))
return msg
@@ -2924,18 +2960,19 @@ class HttpCli(object):
mime = "text/plain; charset=utf-8"
self.out_headers["Accept-Ranges"] = "bytes"
self.send_headers(length=upper - lower, status=status, mime=mime)
logmsg += unicode(status) + logtail
if self.mode == "HEAD" or not do_send:
if self.do_log:
self.log(logmsg)
self.send_headers(length=upper - lower, status=status, mime=mime)
return True
ret = True
with open_func(*open_args) as f:
self.send_headers(length=upper - lower, status=status, mime=mime)
sendfun = sendfile_kern if use_sendfile else sendfile_py
remains = sendfun(
self.log, lower, upper, f, self.s, self.args.s_wr_sz, self.args.s_wr_slp
@@ -2943,7 +2980,7 @@ class HttpCli(object):
if remains > 0:
logmsg += " \033[31m" + unicode(upper - remains) + "\033[0m"
self.keepalive = False
ret = False
spd = self._spd((upper - lower) - remains)
if self.do_log:
@@ -2959,7 +2996,6 @@ class HttpCli(object):
vn: VFS,
rem: str,
items: list[str],
dots: bool,
) -> bool:
if self.args.no_zip:
raise Pebkac(400, "not enabled")
@@ -3016,7 +3052,7 @@ class HttpCli(object):
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
fgen = vn.zipgen(
vpath, rem, set(items), self.uname, dots, False, not self.args.no_scandir
vpath, rem, set(items), self.uname, False, not self.args.no_scandir
)
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
cfmt = ""
@@ -3299,7 +3335,7 @@ class HttpCli(object):
if v == "y":
dur = 86400 * 299
else:
dur = None
dur = 0
v = "x"
ck = gencookie("k304", v, self.args.R, False, dur)
@@ -3309,7 +3345,7 @@ class HttpCli(object):
def setck(self) -> bool:
k, v = self.uparam["setck"].split("=", 1)
t = None if v == "" else 86400 * 299
t = 0 if v == "" else 86400 * 299
ck = gencookie(k, v, self.args.R, False, t)
self.out_headerlist.append(("Set-Cookie", ck))
self.reply(b"o7\n")
@@ -3317,7 +3353,7 @@ class HttpCli(object):
def set_cfg_reset(self) -> bool:
for k in ("k304", "js", "idxh", "cppwd", "cppws"):
cookie = gencookie(k, "x", self.args.R, False, None)
cookie = gencookie(k, "x", self.args.R, False)
self.out_headerlist.append(("Set-Cookie", cookie))
self.redirect("", "?h#cc")
@@ -3327,11 +3363,19 @@ class HttpCli(object):
rc = 404
if self.args.vague_403:
t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try logging in or <a href="{}/?h">go home</a></p>'
pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try logging in)"
elif is_403:
t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">you\'ll have to log in or <a href="{}/?h">go home</a></p>'
pt = "403 forbiddena ~┻━┻ (you'll have to log in)"
rc = 403
else:
t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id="r" href="{}/?h">go home</a></p>'
pt = "404 not found ┐( ´ -`)┌"
if self.ua.startswith("curl/") or self.ua.startswith("fetch"):
pt = "# acct: %s\n%s" % (self.uname, pt)
self.reply(pt.encode("utf-8"), status=rc)
return True
t = t.format(self.args.SR)
qv = quotep(self.vpaths) + self.ourlq()
@@ -3430,6 +3474,7 @@ class HttpCli(object):
ret["k" + quotep(excl)] = sub
vfs = self.asrv.vfs
dots = False
try:
vn, rem = vfs.get(top, self.uname, True, False)
fsroot, vfs_ls, vfs_virt = vn.ls(
@@ -3438,6 +3483,7 @@ class HttpCli(object):
not self.args.no_scandir,
[[True, False], [False, True]],
)
dots = self.uname in vn.axs.udot
except:
vfs_ls = []
vfs_virt = {}
@@ -3446,15 +3492,12 @@ class HttpCli(object):
if d1 == top:
vfs_virt[d2] = vfs # typechk, value never read
dirs = []
dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
dirnames = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
if not dots or "dots" not in self.uparam:
dirs = exclude_dotfiles(dirs)
if not self.args.ed or "dots" not in self.uparam:
dirnames = exclude_dotfiles(dirnames)
for fn in [x for x in dirnames if x != excl]:
dirs.append(quotep(fn))
dirs = [quotep(x) for x in dirs if x != excl]
for x in vfs_virt:
if x != excl:
@@ -3486,7 +3529,8 @@ class HttpCli(object):
fk_vols = {
vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
for vp, vol in self.asrv.vfs.all_vols.items()
if "fk" in vol.flags and (vp in self.rvol or vp in self.upvol)
if "fk" in vol.flags
and (self.uname in vol.axs.uread or self.uname in vol.axs.upget)
}
for vol in self.asrv.vfs.all_vols.values():
cur = idx.get_cur(vol.realpath)
@@ -3757,7 +3801,7 @@ class HttpCli(object):
elif self.can_get and self.avn:
axs = self.avn.axs
if self.uname not in axs.uhtml and "*" not in axs.uhtml:
if self.uname not in axs.uhtml:
pass
elif is_dir:
for fn in ("index.htm", "index.html"):
@@ -3797,7 +3841,8 @@ class HttpCli(object):
)[: vn.flags["fk"]]
got = self.uparam.get("k")
if got != correct:
self.log("wrong filekey, want {}, got {}".format(correct, got))
t = "wrong filekey, want %s, got %s\n vp: %s\n ap: %s"
self.log(t % (correct, got, self.req, abspath), 6)
return self.tx_404()
if (
@@ -3929,6 +3974,7 @@ class HttpCli(object):
"dsort": vf["sort"],
"themes": self.args.themes,
"turbolvl": self.args.turbo,
"u2j": self.args.u2j,
"idxh": int(self.args.ih),
"u2sort": self.args.u2sort,
}
@@ -3978,7 +4024,7 @@ class HttpCli(object):
for k in ["zip", "tar"]:
v = self.uparam.get(k)
if v is not None:
return self.tx_zip(k, v, self.vpath, vn, rem, [], self.args.ed)
return self.tx_zip(k, v, self.vpath, vn, rem, [])
fsroot, vfs_ls, vfs_virt = vn.ls(
rem,
@@ -4009,13 +4055,13 @@ class HttpCli(object):
pass
# show dotfiles if permitted and requested
if not self.args.ed or (
if not self.can_dot or (
"dots" not in self.uparam and (is_ls or "dots" not in self.cookies)
):
ls_names = exclude_dotfiles(ls_names)
add_fk = vn.flags.get("fk")
fk_alg = 2 if "fka" in vn.flags else 1
add_fk = vf.get("fk")
fk_alg = 2 if "fka" in vf else 1
dirs = []
files = []

View File

@@ -112,32 +112,30 @@ class HttpConn(object):
return self.u2idx
def _detect_https(self) -> bool:
method = None
if True:
try:
method = self.s.recv(4, socket.MSG_PEEK)
except socket.timeout:
return False
except AttributeError:
# jython does not support msg_peek; forget about https
method = self.s.recv(4)
self.sr = Util.Unrecv(self.s, self.log)
self.sr.buf = method
try:
method = self.s.recv(4, socket.MSG_PEEK)
except socket.timeout:
return False
except AttributeError:
# jython does not support msg_peek; forget about https
method = self.s.recv(4)
self.sr = Util.Unrecv(self.s, self.log)
self.sr.buf = method
# jython used to do this, they stopped since it's broken
# but reimplementing sendall is out of scope for now
if not getattr(self.s, "sendall", None):
self.s.sendall = self.s.send # type: ignore
# jython used to do this, they stopped since it's broken
# but reimplementing sendall is out of scope for now
if not getattr(self.s, "sendall", None):
self.s.sendall = self.s.send # type: ignore
if len(method) != 4:
err = "need at least 4 bytes in the first packet; got {}".format(
len(method)
)
if method:
self.log(err)
if len(method) != 4:
err = "need at least 4 bytes in the first packet; got {}".format(
len(method)
)
if method:
self.log(err)
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
return False
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
return False
return not method or not bool(PTN_HTTP.match(method))

View File

@@ -109,6 +109,7 @@ class HttpSrv(object):
self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False)
self.gmal = Garda(self.args.ban_422)
self.gurl = Garda(self.args.ban_url)
self.bans: dict[str, int] = {}
self.aclose: dict[str, int] = {}

View File

@@ -8,7 +8,7 @@ import re
from .__init__ import PY2
from .th_srv import HAVE_PIL, HAVE_PILF
from .util import BytesIO
from .util import BytesIO # type: ignore
class Ico(object):
@@ -22,7 +22,7 @@ class Ico(object):
ext = bext.decode("utf-8")
zb = hashlib.sha1(bext).digest()[2:4]
if PY2:
zb = [ord(x) for x in zb]
zb = [ord(x) for x in zb] # type: ignore
c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3)
c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 0.8 if HAVE_PILF else 1, 1)
@@ -91,20 +91,6 @@ class Ico(object):
img.save(buf, format="PNG", compress_level=1)
return "image/png", buf.getvalue()
elif False:
# 48s, too slow
import pyvips
h = int(192 * h / w)
w = 192
img = pyvips.Image.text(
ext, width=w, height=h, dpi=192, align=pyvips.Align.CENTRE
)
img = img.ifthenelse(ci[3:], ci[:3], blend=True)
# i = i.resize(3, kernel=pyvips.Kernel.NEAREST)
buf = img.write_to_buffer(".png[compression=1]")
return "image/png", buf
svg = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>

View File

@@ -118,7 +118,7 @@ def ffprobe(
b"--",
fsenc(abspath),
]
rc, so, se = runcmd(cmd, timeout=timeout)
rc, so, se = runcmd(cmd, timeout=timeout, nice=True)
retchk(rc, cmd, se)
return parse_ffprobe(so)
@@ -261,7 +261,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
if ".resw" in ret and ".resh" in ret:
ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"])
zd = {k: (0, v) for k, v in ret.items()}
zero = int("0")
zd = {k: (zero, v) for k, v in ret.items()}
return zd, md
@@ -562,6 +563,7 @@ class MTag(object):
args = {
"env": env,
"nice": True,
"timeout": parser.timeout,
"kill": parser.kill,
"capture": parser.capture,
@@ -572,11 +574,6 @@ class MTag(object):
zd.update(ret)
args["sin"] = json.dumps(zd).encode("utf-8", "replace")
if WINDOWS:
args["creationflags"] = 0x4000
else:
cmd = ["nice"] + cmd
bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])]
rc, v, err = runcmd(bcmd, **args) # type: ignore
retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v)

View File

@@ -406,6 +406,7 @@ class SMB(object):
smbserver.os.path.abspath = self._hook
smbserver.os.path.expanduser = self._hook
smbserver.os.path.expandvars = self._hook
smbserver.os.path.getatime = self._hook
smbserver.os.path.getctime = self._hook
smbserver.os.path.getmtime = self._hook

View File

@@ -61,7 +61,7 @@ class Adapter(object):
)
if True:
if True: # pylint: disable=using-constant-test
# Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format)
_IPv4Address = str

View File

@@ -138,7 +138,8 @@ class SvcHub(object):
self.gpwd = Garda(self.args.ban_pw)
self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422)
self.g422 = Garda(self.args.ban_422, False)
self.gmal = Garda(self.args.ban_422)
self.gurl = Garda(self.args.ban_url)
self.log_div = 10 ** (6 - args.log_tdec)
@@ -404,20 +405,25 @@ class SvcHub(object):
if al.rsp_jtr:
al.rsp_slp = 0.000001
al.th_covers = set(al.th_covers.split(","))
zsl = al.th_covers.split(",")
zsl = [x.strip() for x in zsl]
zsl = [x for x in zsl if x]
al.th_covers = set(zsl)
al.th_coversd = set(zsl + ["." + x for x in zsl])
for k in "c".split(" "):
vl = getattr(al, k)
if not vl:
continue
vl = [os.path.expanduser(x) if x.startswith("~") else x for x in vl]
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
setattr(al, k, vl)
for k in "lo hist ssl_log".split(" "):
vs = getattr(al, k)
if vs and vs.startswith("~"):
setattr(al, k, os.path.expanduser(vs))
if vs:
vs = os.path.expandvars(os.path.expanduser(vs))
setattr(al, k, vs)
for k in "sus_urls nonsus_urls".split(" "):
vs = getattr(al, k)
@@ -431,11 +437,13 @@ class SvcHub(object):
elif al.ban_url == "no":
al.sus_urls = None
if al.xff_src in ("any", "0", ""):
al.xff_re = None
else:
zs = al.xff_src.replace(" ", "").replace(".", "\\.").replace(",", "|")
al.xff_re = re.compile("^(?:" + zs + ")")
al.xff_hdr = al.xff_hdr.lower()
al.idp_h_usr = al.idp_h_usr.lower()
# al.idp_h_grp = al.idp_h_grp.lower()
al.xff_re = self._ipa2re(al.xff_src)
al.ipa_re = self._ipa2re(al.ipa)
al.ftp_ipa_re = self._ipa2re(al.ftp_ipa or al.ipa)
mte = ODict.fromkeys(DEF_MTE.split(","), True)
al.mte = odfusion(mte, al.mte)
@@ -454,6 +462,13 @@ class SvcHub(object):
return True
def _ipa2re(self, txt) -> Optional[re.Pattern]:
if txt in ("any", "0", ""):
return None
zs = txt.replace(" ", "").replace(".", "\\.").replace(",", "|")
return re.compile("^(?:" + zs + ")")
def _setlimits(self) -> None:
try:
import resource
@@ -516,12 +531,17 @@ class SvcHub(object):
sel_fn = "{}.{}".format(fn, ctr)
fn = sel_fn
try:
os.makedirs(os.path.dirname(fn))
except:
pass
try:
if do_xz:
import lzma
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
self.args.no_logflush = True
else:
lh = open(fn, "wt", encoding="utf-8", errors="replace")
except:
@@ -751,7 +771,24 @@ class SvcHub(object):
(zd.hour * 100 + zd.minute) * 100 + zd.second,
zd.microsecond // self.log_div,
)
self.logf.write("@%s [%s\033[0m] %s\n" % (ts, src, msg))
if c and not self.args.no_ansi:
if isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg)
elif "\033" not in c:
msg = "\033[%sm%s\033[0m" % (c, msg)
else:
msg = "%s%s\033[0m" % (c, msg)
if "\033" in src:
src += "\033[0m"
if "\033" in msg:
msg += "\033[0m"
self.logf.write("@%s [%-21s] %s\n" % (ts, src, msg))
if not self.args.no_logflush:
self.logf.flush()
now = time.time()
if now >= self.next_day:
@@ -827,6 +864,8 @@ class SvcHub(object):
if self.logf:
self.logf.write(msg)
if not self.args.no_logflush:
self.logf.flush()
def pr(self, *a: Any, **ka: Any) -> None:
try:

View File

@@ -241,6 +241,11 @@ class TcpSrv(object):
raise OSError(E_ADDR_IN_USE[0], "")
self.srv.append(srv)
except (OSError, socket.error) as ex:
try:
srv.close()
except:
pass
if ex.errno in E_ADDR_IN_USE:
e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip)
elif ex.errno in E_ADDR_NOT_AVAIL:

View File

@@ -18,7 +18,7 @@ from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
from .util import (
FFMPEG_URL,
BytesIO,
BytesIO, # type: ignore
Cooldown,
Daemon,
Pebkac,
@@ -411,6 +411,7 @@ class ThumbSrv(object):
if c == crops[-1]:
raise
assert img # type: ignore
img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
@@ -468,7 +469,7 @@ class ThumbSrv(object):
def _run_ff(self, cmd: list[bytes], vn: VFS) -> None:
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"])
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True)
if not ret:
return

View File

@@ -9,7 +9,7 @@ import time
from operator import itemgetter
from .__init__ import ANYWIN, TYPE_CHECKING, unicode
from .authsrv import LEELOO_DALLAS
from .authsrv import LEELOO_DALLAS, VFS
from .bos import bos
from .up2k import up2k_wark_from_hashlist
from .util import (
@@ -63,7 +63,7 @@ class U2idx(object):
self.log_func("u2idx", msg, c)
def fsearch(
self, vols: list[tuple[str, str, dict[str, Any]]], body: dict[str, Any]
self, uname: str, vols: list[VFS], body: dict[str, Any]
) -> list[dict[str, Any]]:
"""search by up2k hashlist"""
if not HAVE_SQLITE3:
@@ -77,7 +77,7 @@ class U2idx(object):
uv: list[Union[str, int]] = [wark[:16], wark]
try:
return self.run_query(vols, uq, uv, True, False, 99999)[0]
return self.run_query(uname, vols, uq, uv, False, 99999)[0]
except:
raise Pebkac(500, min_ex())
@@ -103,7 +103,7 @@ class U2idx(object):
uri = ""
try:
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
db = sqlite3.connect(uri, 2, uri=True, check_same_thread=False)
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
cur = db.cursor()
cur.execute('pragma table_info("up")').fetchone()
self.log("ro: {}".format(db_path))
@@ -115,14 +115,14 @@ class U2idx(object):
if not cur:
# on windows, this steals the write-lock from up2k.deferred_init --
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
cur = sqlite3.connect(db_path, 2, check_same_thread=False).cursor()
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
self.log("opened {}".format(db_path))
self.cur[ptop] = cur
return cur
def search(
self, vols: list[tuple[str, str, dict[str, Any]]], uq: str, lim: int
self, uname: str, vols: list[VFS], uq: str, lim: int
) -> tuple[list[dict[str, Any]], list[str], bool]:
"""search by query params"""
if not HAVE_SQLITE3:
@@ -131,7 +131,6 @@ class U2idx(object):
q = ""
v: Union[str, int] = ""
va: list[Union[str, int]] = []
have_up = False # query has up.* operands
have_mt = False
is_key = True
is_size = False
@@ -176,26 +175,21 @@ class U2idx(object):
if v == "size":
v = "up.sz"
is_size = True
have_up = True
elif v == "date":
v = "up.mt"
is_date = True
have_up = True
elif v == "up_at":
v = "up.at"
is_date = True
have_up = True
elif v == "path":
v = "trim(?||up.rd,'/')"
va.append("\nrd")
have_up = True
elif v == "name":
v = "up.fn"
have_up = True
elif v == "tags" or ptn_mt.match(v):
have_mt = True
@@ -271,22 +265,22 @@ class U2idx(object):
q += " lower({}) {} ? ) ".format(field, oper)
try:
return self.run_query(vols, q, va, have_up, have_mt, lim)
return self.run_query(uname, vols, q, va, have_mt, lim)
except Exception as ex:
raise Pebkac(500, repr(ex))
def run_query(
self,
vols: list[tuple[str, str, dict[str, Any]]],
uname: str,
vols: list[VFS],
uq: str,
uv: list[Union[str, int]],
have_up: bool,
have_mt: bool,
lim: int,
) -> tuple[list[dict[str, Any]], list[str], bool]:
if self.args.srch_dbg:
t = "searching across all %s volumes in which the user has 'r' (full read access):\n %s"
zs = "\n ".join(["/%s = %s" % (x[0], x[1]) for x in vols])
zs = "\n ".join(["/%s = %s" % (x.vpath, x.realpath) for x in vols])
self.log(t % (len(vols), zs), 5)
done_flag: list[bool] = []
@@ -307,12 +301,22 @@ class U2idx(object):
ret = []
seen_rps: set[str] = set()
lim = min(lim, int(self.args.srch_hits))
clamp = int(self.args.srch_hits)
if lim >= clamp:
lim = clamp
clamped = True
else:
clamped = False
taglist = {}
for (vtop, ptop, flags) in vols:
for vol in vols:
if lim < 0:
break
vtop = vol.vpath
ptop = vol.realpath
flags = vol.flags
cur = self.get_cur(ptop)
if not cur:
continue
@@ -337,7 +341,7 @@ class U2idx(object):
sret = []
fk = flags.get("fk")
dots = flags.get("dotsrch")
dots = flags.get("dotsrch") and uname in vol.axs.udot
fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv))
for hit in c:
@@ -420,7 +424,7 @@ class U2idx(object):
ret.sort(key=itemgetter("rp"))
return ret, list(taglist.keys()), lim < 0
return ret, list(taglist.keys()), lim < 0 and not clamped
def terminator(self, identifier: str, done_flag: list[bool]) -> None:
for _ in range(self.timeout):

View File

@@ -419,50 +419,49 @@ class Up2k(object):
def _check_lifetimes(self) -> float:
now = time.time()
timeout = now + 9001
if now: # diff-golf
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
lifetime = vol.flags.get("lifetime")
if not lifetime:
continue
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
lifetime = vol.flags.get("lifetime")
if not lifetime:
continue
cur = self.cur.get(vol.realpath)
if not cur:
continue
cur = self.cur.get(vol.realpath)
if not cur:
continue
nrm = 0
deadline = time.time() - lifetime
timeout = min(timeout, now + lifetime)
q = "select rd, fn from up where at > 0 and at < ? limit 100"
while True:
with self.mutex:
hits = cur.execute(q, (deadline,)).fetchall()
if not hits:
break
for rd, fn in hits:
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
fvp = ("%s/%s" % (rd, fn)).strip("/")
if vp:
fvp = "%s/%s" % (vp, fvp)
self._handle_rm(LEELOO_DALLAS, "", fvp, [], True)
nrm += 1
if nrm:
self.log("{} files graduated in {}".format(nrm, vp))
if timeout < 10:
continue
q = "select at from up where at > 0 order by at limit 1"
nrm = 0
deadline = time.time() - lifetime
timeout = min(timeout, now + lifetime)
q = "select rd, fn from up where at > 0 and at < ? limit 100"
while True:
with self.mutex:
hits = cur.execute(q).fetchone()
hits = cur.execute(q, (deadline,)).fetchall()
if hits:
timeout = min(timeout, now + lifetime - (now - hits[0]))
if not hits:
break
for rd, fn in hits:
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
fvp = ("%s/%s" % (rd, fn)).strip("/")
if vp:
fvp = "%s/%s" % (vp, fvp)
self._handle_rm(LEELOO_DALLAS, "", fvp, [], True)
nrm += 1
if nrm:
self.log("{} files graduated in {}".format(nrm, vp))
if timeout < 10:
continue
q = "select at from up where at > 0 order by at limit 1"
with self.mutex:
hits = cur.execute(q).fetchone()
if hits:
timeout = min(timeout, now + lifetime - (now - hits[0]))
return timeout
@@ -882,7 +881,9 @@ class Up2k(object):
try:
if bos.makedirs(histpath):
hidedir(histpath)
except:
except Exception as ex:
t = "failed to initialize volume '/%s': %s"
self.log(t % (vpath, ex), 1)
return None
try:
@@ -1117,7 +1118,11 @@ class Up2k(object):
if stat.S_ISDIR(inf.st_mode):
rap = absreal(abspath)
if dev and inf.st_dev != dev:
if (
dev
and inf.st_dev != dev
and not (ANYWIN and bos.stat(rap).st_dev == dev)
):
self.log("skip xdev {}->{}: {}".format(dev, inf.st_dev, abspath), 6)
continue
if abspath in excl or rap in excl:
@@ -1164,8 +1169,12 @@ class Up2k(object):
files.append((sz, lmod, iname))
liname = iname.lower()
if sz and (
iname in self.args.th_covers
or (not cv and liname.rsplit(".", 1)[-1] in CV_EXTS)
iname in self.args.th_coversd
or (
not cv
and liname.rsplit(".", 1)[-1] in CV_EXTS
and not iname.startswith(".")
)
):
cv = iname
@@ -1215,76 +1224,74 @@ class Up2k(object):
abspath = os.path.join(cdir, fn)
nohash = reh.search(abspath) if reh else False
if fn: # diff-golf
sql = "select w, mt, sz, at from up where rd = ? and fn = ?"
try:
c = db.c.execute(sql, (rd, fn))
except:
c = db.c.execute(sql, s3enc(self.mem_cur, rd, fn))
sql = "select w, mt, sz, at from up where rd = ? and fn = ?"
try:
c = db.c.execute(sql, (rd, fn))
except:
c = db.c.execute(sql, s3enc(self.mem_cur, rd, fn))
in_db = list(c.fetchall())
if in_db:
self.pp.n -= 1
dw, dts, dsz, at = in_db[0]
if len(in_db) > 1:
t = "WARN: multiple entries: [{}] => [{}] |{}|\n{}"
rep_db = "\n".join([repr(x) for x in in_db])
self.log(t.format(top, rp, len(in_db), rep_db))
dts = -1
in_db = list(c.fetchall())
if in_db:
self.pp.n -= 1
dw, dts, dsz, at = in_db[0]
if len(in_db) > 1:
t = "WARN: multiple entries: [{}] => [{}] |{}|\n{}"
rep_db = "\n".join([repr(x) for x in in_db])
self.log(t.format(top, rp, len(in_db), rep_db))
dts = -1
if fat32 and abs(dts - lmod) == 1:
dts = lmod
if fat32 and abs(dts - lmod) == 1:
dts = lmod
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
continue
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
continue
t = "reindex [{}] => [{}] ({}/{}) ({}/{})".format(
top, rp, dts, lmod, dsz, sz
)
self.log(t)
self.db_rm(db.c, rd, fn, 0)
ret += 1
db.n += 1
in_db = []
else:
at = 0
self.pp.msg = "a{} {}".format(self.pp.n, abspath)
if nohash or not sz:
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
else:
if sz > 1024 * 1024:
self.log("file: {}".format(abspath))
try:
hashes = self._hashlist_from_file(
abspath, "a{}, ".format(self.pp.n)
)
except Exception as ex:
self.log("hash: {} @ [{}]".format(repr(ex), abspath))
continue
if not hashes:
return -1
wark = up2k_wark_from_hashlist(self.salt, sz, hashes)
# skip upload hooks by not providing vflags
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, "", "", "", at)
db.n += 1
t = "reindex [{}] => [{}] ({}/{}) ({}/{})".format(
top, rp, dts, lmod, dsz, sz
)
self.log(t)
self.db_rm(db.c, rd, fn, 0)
ret += 1
td = time.time() - db.t
if db.n >= 4096 or td >= 60:
self.log("commit {} new files".format(db.n))
db.c.connection.commit()
db.n = 0
db.t = time.time()
db.n += 1
in_db = []
else:
at = 0
self.pp.msg = "a{} {}".format(self.pp.n, abspath)
if nohash or not sz:
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
else:
if sz > 1024 * 1024:
self.log("file: {}".format(abspath))
try:
hashes = self._hashlist_from_file(
abspath, "a{}, ".format(self.pp.n)
)
except Exception as ex:
self.log("hash: {} @ [{}]".format(repr(ex), abspath))
continue
if not hashes:
return -1
wark = up2k_wark_from_hashlist(self.salt, sz, hashes)
# skip upload hooks by not providing vflags
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, "", "", "", at)
db.n += 1
ret += 1
td = time.time() - db.t
if db.n >= 4096 or td >= 60:
self.log("commit {} new files".format(db.n))
db.c.connection.commit()
db.n = 0
db.t = time.time()
if not self.args.no_dhash:
db.c.execute("delete from dh where d = ?", (drd,))
db.c.execute("insert into dh values (?,?)", (drd, dhash))
db.c.execute("delete from dh where d = ?", (drd,)) # type: ignore
db.c.execute("insert into dh values (?,?)", (drd, dhash)) # type: ignore
if self.stop:
return -1
@@ -1303,7 +1310,7 @@ class Up2k(object):
if n:
t = "forgetting {} shadowed autoindexed files in [{}] > [{}]"
self.log(t.format(n, top, sh_rd))
assert sh_erd
assert sh_erd # type: ignore
q = "delete from dh where (d = ? or d like ?||'%')"
db.c.execute(q, (sh_erd, sh_erd + "/"))
@@ -2138,7 +2145,9 @@ class Up2k(object):
self.log("ST: {}".format(msg))
def _orz(self, db_path: str) -> "sqlite3.Cursor":
c = sqlite3.connect(db_path, self.timeout, check_same_thread=False).cursor()
c = sqlite3.connect(
db_path, timeout=self.timeout, check_same_thread=False
).cursor()
# c.connection.set_trace_callback(self._trace)
return c
@@ -2202,7 +2211,7 @@ class Up2k(object):
t = "native sqlite3 backup failed; using fallback method:\n"
self.log(t + min_ex())
finally:
c2.close()
c2.close() # type: ignore
db = cur.connection
cur.close()
@@ -2659,12 +2668,7 @@ class Up2k(object):
not ret["hash"]
and "fk" in vfs.flags
and not self.args.nw
and (
cj["user"] in vfs.axs.uread
or cj["user"] in vfs.axs.upget
or "*" in vfs.axs.uread
or "*" in vfs.axs.upget
)
and (cj["user"] in vfs.axs.uread or cj["user"] in vfs.axs.upget)
):
alg = 2 if "fka" in vfs.flags else 1
ap = absreal(djoin(job["ptop"], job["prel"], job["name"]))
@@ -2929,7 +2933,6 @@ class Up2k(object):
self._symlink(dst, d2, self.flags[ptop], lmod=lmod)
if cur:
self.db_rm(cur, rd, fn, job["size"])
self.db_add(cur, vflags, rd, fn, lmod, *z2[3:])
if cur:
@@ -2972,7 +2975,6 @@ class Up2k(object):
self.db_act = self.vol_act[ptop] = time.time()
try:
self.db_rm(cur, rd, fn, sz)
self.db_add(
cur,
vflags,
@@ -3031,6 +3033,8 @@ class Up2k(object):
at: float,
skip_xau: bool = False,
) -> None:
self.db_rm(db, rd, fn, sz)
sql = "insert into up values (?,?,?,?,?,?,?)"
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
try:
@@ -3087,7 +3091,7 @@ class Up2k(object):
with self.rescan_cond:
self.rescan_cond.notify_all()
if rd and sz and fn.lower() in self.args.th_covers:
if rd and sz and fn.lower() in self.args.th_coversd:
# wasteful; db_add will re-index actual covers
# but that won't catch existing files
crd, cdn = rd.rsplit("/", 1) if "/" in rd else ("", rd)
@@ -3192,7 +3196,13 @@ class Up2k(object):
break
abspath = djoin(adir, fn)
st = bos.stat(abspath)
st = stl = bos.lstat(abspath)
if stat.S_ISLNK(st.st_mode):
try:
st = bos.stat(abspath)
except:
pass
volpath = "{}/{}".format(vrem, fn).strip("/")
vpath = "{}/{}".format(dbv.vpath, volpath).strip("/")
self.log("rm {}\n {}".format(vpath, abspath))
@@ -3205,7 +3215,7 @@ class Up2k(object):
vpath,
"",
uname,
st.st_mtime,
stl.st_mtime,
st.st_size,
ip,
0,
@@ -3235,7 +3245,7 @@ class Up2k(object):
vpath,
"",
uname,
st.st_mtime,
stl.st_mtime,
st.st_size,
ip,
0,
@@ -3352,28 +3362,27 @@ class Up2k(object):
if bos.path.exists(dabs):
raise Pebkac(400, "mv2: target file exists")
stl = bos.lstat(sabs)
try:
st = bos.stat(sabs)
except:
st = stl
is_link = is_dirlink = False
st = stl = bos.lstat(sabs)
if stat.S_ISLNK(stl.st_mode):
is_link = True
try:
st = bos.stat(sabs)
is_dirlink = stat.S_ISDIR(st.st_mode)
except:
pass # broken symlink; keep as-is
xbr = svn.flags.get("xbr")
xar = dvn.flags.get("xar")
if xbr:
if not runhook(
self.log, xbr, sabs, svp, "", uname, st.st_mtime, st.st_size, "", 0, ""
self.log, xbr, sabs, svp, "", uname, stl.st_mtime, st.st_size, "", 0, ""
):
t = "move blocked by xbr server config: {}".format(svp)
self.log(t, 1)
raise Pebkac(405, t)
is_xvol = svn.realpath != dvn.realpath
if stat.S_ISLNK(stl.st_mode):
is_dirlink = stat.S_ISDIR(st.st_mode)
is_link = True
else:
is_link = is_dirlink = False
bos.makedirs(os.path.dirname(dabs))
@@ -3400,7 +3409,7 @@ class Up2k(object):
c2 = self.cur.get(dvn.realpath)
if ftime_ is None:
ftime = st.st_mtime
ftime = stl.st_mtime
fsize = st.st_size
else:
ftime = ftime_
@@ -3442,7 +3451,16 @@ class Up2k(object):
if is_xvol and has_dupes:
raise OSError(errno.EXDEV, "src is symlink")
atomic_move(sabs, dabs)
if is_link and st != stl:
# relink non-broken symlinks to still work after the move,
# but only resolve 1st level to maintain relativity
dlink = bos.readlink(sabs)
dlink = os.path.join(os.path.dirname(sabs), dlink)
dlink = bos.path.abspath(dlink)
self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
bos.unlink(sabs)
else:
atomic_move(sabs, dabs)
except OSError as ex:
if ex.errno != errno.EXDEV:
@@ -3609,6 +3627,8 @@ class Up2k(object):
except:
self.log("relink: not found: [{}]".format(ap))
# self.log("full:\n" + "\n".join(" {:90}: {}".format(*x) for x in full.items()))
# self.log("links:\n" + "\n".join(" {:90}: {}".format(*x) for x in links.items()))
if not dabs and not full and links:
# deleting final remaining full copy; swap it with a symlink
slabs = list(sorted(links.keys()))[0]
@@ -3626,12 +3646,45 @@ class Up2k(object):
dabs = list(sorted(full.keys()))[0]
for alink, parts in links.items():
lmod = None
lmod = 0.0
try:
if alink != sabs and absreal(alink) != sabs:
continue
faulty = False
ldst = alink
try:
for n in range(40): # MAXSYMLINKS
zs = bos.readlink(ldst)
ldst = os.path.join(os.path.dirname(ldst), zs)
ldst = bos.path.abspath(ldst)
if not bos.path.islink(ldst):
break
self.log("relinking [{}] to [{}]".format(alink, dabs))
if ldst == sabs:
t = "relink because level %d would break:"
self.log(t % (n,), 6)
faulty = True
except Exception as ex:
self.log("relink because walk failed: %s; %r" % (ex, ex), 3)
faulty = True
zs = absreal(alink)
if ldst != zs:
t = "relink because computed != actual destination:\n %s\n %s"
self.log(t % (ldst, zs), 3)
ldst = zs
faulty = True
if bos.path.islink(ldst):
raise Exception("broken symlink: %s" % (alink,))
if alink != sabs and ldst != sabs and not faulty:
continue # original symlink OK; leave it be
except Exception as ex:
t = "relink because symlink verification failed: %s; %r"
self.log(t % (ex, ex), 3)
self.log("relinking [%s] to [%s]" % (alink, dabs))
try:
lmod = bos.path.getmtime(alink, False)
bos.unlink(alink)
except:

View File

@@ -115,6 +115,11 @@ if True: # pylint: disable=using-constant-test
import typing
from typing import Any, Generator, Optional, Pattern, Protocol, Union
try:
from typing import LiteralString
except:
pass
class RootLogger(Protocol):
def __call__(self, src: str, msg: str, c: Union[int, str] = 0) -> None:
return None
@@ -144,15 +149,15 @@ if not PY2:
from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote
else:
from StringIO import StringIO as BytesIO
from urllib import quote # pylint: disable=no-name-in-module
from urllib import unquote # pylint: disable=no-name-in-module
from StringIO import StringIO as BytesIO # type: ignore
from urllib import quote # type: ignore # pylint: disable=no-name-in-module
from urllib import unquote # type: ignore # pylint: disable=no-name-in-module
try:
struct.unpack(b">i", b"idgi")
spack = struct.pack
sunpack = struct.unpack
spack = struct.pack # type: ignore
sunpack = struct.unpack # type: ignore
except:
def spack(fmt: bytes, *a: Any) -> bytes:
@@ -378,6 +383,7 @@ def py_desc() -> str:
def _sqlite_ver() -> str:
assert sqlite3 # type: ignore
try:
co = sqlite3.connect(":memory:")
cur = co.cursor()
@@ -1068,6 +1074,17 @@ def nuprint(msg: str) -> None:
uprint("{}\n".format(msg))
def dedent(txt: str) -> str:
pad = 64
lns = txt.replace("\r", "").split("\n")
for ln in lns:
zs = ln.lstrip()
pad2 = len(ln) - len(zs)
if zs and pad > pad2:
pad = pad2
return "\n".join([ln[pad:] for ln in lns])
def rice_tid() -> str:
tid = threading.current_thread().ident
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])
@@ -1647,16 +1664,15 @@ def gen_filekey_dbg(
return ret
def gencookie(k: str, v: str, r: str, tls: bool, dur: Optional[int]) -> str:
def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") -> str:
v = v.replace("%", "%25").replace(";", "%3B")
if dur:
exp = formatdate(time.time() + dur, usegmt=True)
else:
exp = "Fri, 15 Aug 1997 01:00:00 GMT"
return "{}={}; Path=/{}; Expires={}{}; SameSite=Lax".format(
k, v, r, exp, "; Secure" if tls else ""
)
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax"
return t % (k, v, r, exp, "; Secure" if tls else "", txt)
def humansize(sz: float, terse: bool = False) -> str:
@@ -1818,7 +1834,9 @@ def exclude_dotfiles(filepaths: list[str]) -> list[str]:
return [x for x in filepaths if not x.split("/")[-1].startswith(".")]
def odfusion(base: ODict[str, bool], oth: str) -> ODict[str, bool]:
def odfusion(
base: Union[ODict[str, bool], ODict["LiteralString", bool]], oth: str
) -> ODict[str, bool]:
# merge an "ordered set" (just a dict really) with another list of keys
words0 = [x for x in oth.split(",") if x]
words1 = [x for x in oth[1:].split(",") if x]
@@ -1988,10 +2006,10 @@ else:
# moonrunes become \x3f with bytestrings,
# losing mojibake support is worth
def _not_actually_mbcs_enc(txt: str) -> bytes:
return txt
return txt # type: ignore
def _not_actually_mbcs_dec(txt: bytes) -> str:
return txt
return txt # type: ignore
fsenc = afsenc = sfsenc = _not_actually_mbcs_enc
fsdec = _not_actually_mbcs_dec
@@ -2050,6 +2068,7 @@ def atomic_move(usrc: str, udst: str) -> None:
def get_df(abspath: str) -> tuple[Optional[int], Optional[int]]:
try:
# some fuses misbehave
assert ctypes
if ANYWIN:
bfree = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
@@ -2452,6 +2471,7 @@ def getalive(pids: list[int], pgid: int) -> list[int]:
alive.append(pid)
else:
# windows doesn't have pgroups; assume
assert psutil
psutil.Process(pid)
alive.append(pid)
except:
@@ -2469,6 +2489,7 @@ def killtree(root: int) -> None:
pgid = 0
if HAVE_PSUTIL:
assert psutil
pids = [root]
parent = psutil.Process(root)
for child in parent.children(recursive=True):
@@ -2508,9 +2529,34 @@ def killtree(root: int) -> None:
pass
def _find_nice() -> str:
if WINDOWS:
return "" # use creationflags
try:
zs = shutil.which("nice")
if zs:
return zs
except:
pass
# busted PATHs and/or py2
for zs in ("/bin", "/sbin", "/usr/bin", "/usr/sbin"):
zs += "/nice"
if os.path.exists(zs):
return zs
return ""
NICES = _find_nice()
NICEB = NICES.encode("utf-8")
def runcmd(
argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
) -> tuple[int, str, str]:
isbytes = isinstance(argv[0], (bytes, bytearray))
kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one
capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both
@@ -2524,13 +2570,22 @@ def runcmd(
berr: bytes
if ANYWIN:
if isinstance(argv[0], (bytes, bytearray)):
if isbytes:
if argv[0] in CMD_EXEB:
argv[0] += b".exe"
else:
if argv[0] in CMD_EXES:
argv[0] += ".exe"
if ka.pop("nice", None):
if WINDOWS:
ka["creationflags"] = 0x4000
elif NICEB:
if isbytes:
argv = [NICEB] + argv
else:
argv = [NICES] + argv
p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka)
if not timeout or PY2:
bout, berr = p.communicate(sin)
@@ -2678,13 +2733,13 @@ def _parsehook(
sp_ka = {
"env": env,
"nice": True,
"timeout": tout,
"kill": kill,
"capture": cap,
}
if cmd.startswith("~"):
cmd = os.path.expanduser(cmd)
cmd = os.path.expandvars(os.path.expanduser(cmd))
return chk, fork, jtxt, wait, sp_ka, cmd
@@ -2823,9 +2878,7 @@ def loadpy(ap: str, hot: bool) -> Any:
depending on what other inconveniently named files happen
to be in the same folder
"""
if ap.startswith("~"):
ap = os.path.expanduser(ap)
ap = os.path.expandvars(os.path.expanduser(ap))
mdir, mfile = os.path.split(absreal(ap))
mname = mfile.rsplit(".", 1)[0]
sys.path.insert(0, mdir)
@@ -2833,7 +2886,7 @@ def loadpy(ap: str, hot: bool) -> Any:
if PY2:
mod = __import__(mname)
if hot:
reload(mod)
reload(mod) # type: ignore
else:
import importlib
@@ -2976,6 +3029,7 @@ def termsize() -> tuple[int, int]:
def hidedir(dp) -> None:
if ANYWIN:
try:
assert ctypes
k32 = ctypes.WinDLL("kernel32")
attrs = k32.GetFileAttributesW(dp)
if attrs >= 0:

View File

@@ -327,8 +327,8 @@ var Ls = {
"tv_xe1": "could not load textfile:\n\nerror ",
"tv_xe2": "404, file not found",
"tv_lst": "list of textfiles in",
"tvt_close": "return to folder view$NHotkey: M\">❌ close",
"tvt_dl": "download this file\">💾 download",
"tvt_close": "return to folder view$NHotkey: M (or Esc)\">❌ close",
"tvt_dl": "download this file$NHotkey: Y\">💾 download",
"tvt_prev": "show previous document$NHotkey: i\">⬆ prev",
"tvt_next": "show next document$NHotkey: K\">⬇ next",
"tvt_sel": "select file &nbsp; ( for cut / delete / ... )$NHotkey: S\">sel",
@@ -559,8 +559,9 @@ var Ls = {
"dokumentviser",
["I/K", "forr./neste fil"],
["M", "lukk tekstdokument"],
["E", "rediger tekstdokument"]
["S", "velg fil (for F2/ctrl-x/...)"]
["E", "rediger tekstdokument"],
["S", "velg fil (for F2/ctrl-x/...)"],
["Y", "last ned tekstfil"],
]
],
@@ -806,8 +807,8 @@ var Ls = {
"tv_xe1": "kunne ikke laste tekstfil:\n\nfeil ",
"tv_xe2": "404, Fil ikke funnet",
"tv_lst": "tekstfiler i mappen",
"tvt_close": "gå tilbake til mappen$NSnarvei: M\">❌ lukk",
"tvt_dl": "last ned denne filen\">💾 last ned",
"tvt_close": "gå tilbake til mappen$NSnarvei: M (eller Esc)\">❌ lukk",
"tvt_dl": "last ned denne filen$NSnarvei: Y\">💾 last ned",
"tvt_prev": "vis forrige dokument$NSnarvei: i\">⬆ forr.",
"tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste",
"tvt_sel": "markér filen &nbsp; ( for utklipp / sletting / ... )$NSnarvei: S\">merk",
@@ -1076,7 +1077,7 @@ ebi('op_up2k').innerHTML = (
' <tr>\n' +
' <td class="c" data-perm="read">\n' +
' <a href="#" class="b" id="nthread_sub">&ndash;</a><input\n' +
' class="txtbox" id="nthread" value="2" tt="' + L.ut_par + '"/><a\n' +
' class="txtbox" id="nthread" value="" tt="' + L.ut_par + '"/><a\n' +
' href="#" class="b" id="nthread_add">+</a><br />&nbsp;\n' +
' </td>\n' +
' </tr>\n' +
@@ -1505,7 +1506,6 @@ var mpl = (function () {
artist = (np.circle && np.circle != np.artist ? np.circle + ' // ' : '') + (np.artist || (fns.length > 1 ? fns[0] : '')),
title = np.title || fns.pop(),
cover = '',
pcover = '',
tags = { title: title };
if (artist)
@@ -1520,20 +1520,14 @@ var mpl = (function () {
for (var a = 0, aa = files.length; a < aa; a++) {
if (/^(cover|folder)\.(jpe?g|png|gif)$/i.test(files[a].textContent)) {
cover = noq_href(files[a]);
cover = files[a].getAttribute('href');
break;
}
}
if (cover) {
cover += (cover.indexOf('?') === -1 ? '?' : '&') + 'th=j';
pcover = cover;
var pwd = get_pwd();
if (pwd)
pcover += '&pw=' + uricom_enc(pwd);
tags.artwork = [{ "src": pcover, type: "image/jpeg" }];
tags.artwork = [{ "src": cover, type: "image/jpeg" }];
}
}
@@ -1545,7 +1539,7 @@ var mpl = (function () {
ebi('np_dur').textContent = np['.dur'] || '';
ebi('np_url').textContent = get_vpath() + np.file.split('?')[0];
if (!MOBILE)
ebi('np_img').setAttribute('src', cover || ''); // dont give last.fm the pwd
ebi('np_img').setAttribute('src', cover || '');
navigator.mediaSession.metadata = new MediaMetadata(tags);
navigator.mediaSession.setActionHandler('play', mplay);
@@ -1723,7 +1717,7 @@ function MPlayer() {
var t0 = Date.now();
if (mpl.waves)
fetch(url + '&th=p').then(function (x) {
fetch(url.replace(/\bth=opus&/, '') + '&th=p').then(function (x) {
x.body.getReader().read();
});
@@ -2243,7 +2237,7 @@ function song_skip(n, dirskip) {
return;
}
if (tid)
if (tid && !dirskip)
play(ofs + n);
else
play(mp.order[n == -1 ? mp.order.length - 1 : 0]);
@@ -2276,6 +2270,21 @@ function next_song_cmn(e) {
mpl.traversals = 0;
t_fchg = 0;
}
function last_song(e) {
ev(e);
if (mp.order.length) {
mpl.traversals = 0;
return song_skip(-1, true);
}
if (mpl.traversals++ < 5) {
treectl.ls_cb = last_song;
return tree_neigh(-1);
}
toast.inf(10, L.mm_nof);
console.log("mm_nof2");
mpl.traversals = 0;
t_fchg = 0;
}
function prev_song(e) {
ev(e);
@@ -2920,7 +2929,7 @@ function play(tid, is_ev, seek) {
tn = mp.order.length - 1;
}
else if (mpl.pb_mode == 'next') {
treectl.ls_cb = prev_song;
treectl.ls_cb = last_song;
return tree_neigh(-1);
}
}
@@ -3004,7 +3013,7 @@ function play(tid, is_ev, seek) {
pbar.unwave();
if (mpl.waves)
pbar.loadwaves(url + '&th=p');
pbar.loadwaves(url.replace(/\bth=opus&/, '') + '&th=p');
mpui.progress_updater();
pbar.onresize();
@@ -3647,7 +3656,7 @@ var fileman = (function () {
(function (a) {
f[a].inew.onkeydown = function (e) {
rn_ok(a, true);
if (e.key == 'Enter')
if (e.key.endsWith('Enter'))
return rn_apply();
};
QS('.rn_dec' + k).onclick = function (e) {
@@ -3742,7 +3751,7 @@ var fileman = (function () {
if (e.key == 'Escape')
return rn_cancel();
if (e.key == 'Enter')
if (e.key.endsWith('Enter'))
return rn_apply();
};
@@ -4118,7 +4127,10 @@ var showfile = (function () {
if (lang == 'md' && td.textContent != '-')
continue;
td.innerHTML = '<a href="#" class="doc bri" hl="' + link.id + '">-txt-</a>';
td.innerHTML = '<a href="#" id="t' +
link.id + '" class="doc bri" hl="' +
link.id + '">-txt-</a>';
td.getElementsByTagName('a')[0].setAttribute('href', '?doc=' + fn);
}
r.mktree();
@@ -4212,6 +4224,9 @@ var showfile = (function () {
el.textContent = txt;
el.innerHTML = '<code>' + el.innerHTML + '</code>';
if (!window.no_prism) {
if ((lang == 'conf' || lang == 'cfg') && ('\n' + txt).indexOf('\n# -*- mode: yaml -*-') + 1)
lang = 'yaml';
el.className = 'prism linkable-line-numbers line-numbers language-' + lang;
if (!defer)
fun(el.firstChild);
@@ -4447,6 +4462,7 @@ var thegrid = (function () {
filecols.uivis();
aligngriditems();
restore_scroll();
};
r.setdirty = function () {
@@ -4502,7 +4518,10 @@ var thegrid = (function () {
function gclick(e, dbl) {
var oth = ebi(this.getAttribute('ref')),
href = noq_href(this),
aplay = ebi('a' + oth.getAttribute('id')),
fid = oth.getAttribute('id'),
aplay = ebi('a' + fid),
atext = ebi('t' + fid),
is_txt = atext && showfile.getlang(href),
is_img = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4)(\?|$)/i.test(href),
is_dir = href.endsWith('/'),
is_srch = !!ebi('unsearch'),
@@ -4517,15 +4536,21 @@ var thegrid = (function () {
return r.loadsel();
clmod(this, 'sel', clgot(tr, 'sel'));
}
else if (widget.is_open && aplay)
aplay.click();
else if (in_tree && !have_sel)
in_tree.click();
else if (oth.hasAttribute('download'))
oth.click();
else if (widget.is_open && aplay)
aplay.click();
else if (is_dir && !have_sel)
treectl.reqls(href, true);
else if (is_txt && !has(['md', 'htm', 'html'], is_txt))
atext.click();
else if (!is_img && have_sel)
window.open(href, '_blank');
@@ -4975,7 +5000,7 @@ document.onkeydown = function (e) {
}
}
if (k == 'Enter' && ae && (ae.onclick || ae.hasAttribute('tabIndex')))
if (k.endsWith('Enter') && ae && (ae.onclick || ae.hasAttribute('tabIndex')))
return ev(e) && ae.click() || true;
if (aet && aet != 'a' && aet != 'tr' && aet != 'pre')
@@ -5051,6 +5076,13 @@ document.onkeydown = function (e) {
return QS('#twobytwo').click();
}
if (showfile.active()) {
if (k == 'KeyS')
showfile.tglsel();
if (k == 'KeyE' && ebi('editdoc').style.display != 'none')
ebi('editdoc').click();
}
if (thegrid.en) {
if (k == 'KeyS')
return ebi('gridsel').click();
@@ -5061,13 +5093,6 @@ document.onkeydown = function (e) {
if (k == 'KeyD')
return QSA('#ghead a[z]')[1].click();
}
if (showfile.active()) {
if (k == 'KeyS')
showfile.tglsel();
if (k == 'KeyE' && ebi('editdoc').style.display != 'none')
ebi('editdoc').click();
}
};
@@ -5173,7 +5198,7 @@ document.onkeydown = function (e) {
}
function ev_search_keydown(e) {
if (e.key == 'Enter')
if (e.key.endsWith('Enter'))
do_search();
}
@@ -6062,7 +6087,7 @@ var treectl = (function () {
}
if (tn.lead == '-')
tn.lead = '<a href="?doc=' + bhref +
tn.lead = '<a href="?doc=' + bhref + '" id="t' + id +
'" class="doc' + (lang ? ' bri' : '') +
'" hl="' + id + '" name="' + hname + '">-txt-</a>';
@@ -6097,6 +6122,7 @@ var treectl = (function () {
setTimeout(r.tscroll, 100);
}
}
else ebi('lazy').innerHTML = '';
function asdf() {
showfile.mktree();
@@ -7742,10 +7768,36 @@ ebi('path').onclick = function (e) {
};
var scroll_y = -1;
var scroll_vp = '\n';
var scroll_obj = null;
function persist_scroll() {
var obj = scroll_obj;
if (!obj) {
var o1 = document.getElementsByTagName('html')[0];
var o2 = document.body;
obj = o1.scrollTop > o2.scrollTop ? o1 : o2;
}
var y = obj.scrollTop;
if (y > 0)
scroll_obj = obj;
scroll_y = y;
scroll_vp = get_evpath();
}
function restore_scroll() {
if (get_evpath() == scroll_vp && scroll_obj && scroll_obj.scrollTop < 1)
scroll_obj.scrollTop = scroll_y;
}
ebi('files').onclick = ebi('docul').onclick = function (e) {
if (!treectl.csel && e && (ctrl(e) || e.shiftKey))
return true;
if (!showfile.active())
persist_scroll();
var tgt = e.target.closest('a[id]');
if (tgt && tgt.getAttribute('id').indexOf('f-') === 0 && tgt.textContent.endsWith('/')) {
var el = treectl.find(tgt.textContent.slice(0, -1));

View File

@@ -931,7 +931,13 @@ var set_lno = (function () {
// hotkeys / toolbar
(function () {
var keydown = function (ev) {
ev = ev || window.event;
if (!ev && window.event) {
ev = window.event;
if (localStorage.dev_fbw == 1) {
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
console.error('using window.event');
}
}
var kc = ev.code || ev.keyCode || ev.which,
editing = document.activeElement == dom_src;
@@ -1003,7 +1009,7 @@ var set_lno = (function () {
md_home(ev.shiftKey);
return false;
}
if (!ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
if (!ev.shiftKey && (ev.code.endsWith("Enter") || kc == 13)) {
return md_newline();
}
if (!ev.shiftKey && kc == 8) {

View File

@@ -852,7 +852,7 @@ function up2k_init(subtle) {
setmsg(suggest_up2k, 'msg');
var parallel_uploads = icfg_get('nthread'),
var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j),
uc = {},
fdom_ctr = 0,
biggest_file = 0;
@@ -1043,7 +1043,7 @@ function up2k_init(subtle) {
clmod(ebi(v), 'hl', 1);
}
function offdrag(e) {
ev(e);
noope(e);
var v = this.getAttribute('v');
if (v)
@@ -2685,7 +2685,11 @@ function up2k_init(subtle) {
}
parallel_uploads = v;
swrite('nthread', v);
if (v == u2j)
localStorage.removeItem('nthread');
else
swrite('nthread', v);
clmod(obj, 'err');
return;
}
@@ -2699,7 +2703,7 @@ function up2k_init(subtle) {
parallel_uploads = 16;
obj.value = parallel_uploads;
bumpthread({ "target": 1 })
bumpthread({ "target": 1 });
}
function tgl_fsearch() {

View File

@@ -277,7 +277,13 @@ function anymod(e, shift_ok) {
function ev(e) {
e = e || window.event;
if (!e && window.event) {
e = window.event;
if (localStorage.dev_fbw == 1) {
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
console.error('using window.event');
}
}
if (!e)
return;
@@ -296,7 +302,7 @@ function ev(e) {
function noope(e) {
ev(e);
try { ev(e); } catch (ex) { }
}
@@ -755,17 +761,6 @@ function noq_href(el) {
}
function get_pwd() {
var k = HTTPS ? 's=' : 'd=',
pwd = ('; ' + document.cookie).split('; cppw' + k);
if (pwd.length < 2)
return null;
return decodeURIComponent(pwd[1].split(';')[0]);
}
function unix2iso(ts) {
return new Date(ts * 1000).toISOString().replace("T", " ").slice(0, -5);
}
@@ -936,7 +931,7 @@ function fcfg_get(name, defval) {
val = parseFloat(sread(name));
if (!isNum(val))
return parseFloat(o ? o.value : defval);
return parseFloat(o && o.value !== '' ? o.value : defval);
if (o)
o.value = val;
@@ -1473,6 +1468,7 @@ var modal = (function () {
r.load();
r.busy = false;
r.nofocus = 0;
r.show = function (html) {
o = mknod('div', 'modal');
@@ -1486,6 +1482,7 @@ var modal = (function () {
a.onclick = ng;
a = ebi('modal-ok');
a.addEventListener('blur', onblur);
a.onclick = ok;
var inp = ebi('modali');
@@ -1496,6 +1493,7 @@ var modal = (function () {
}, 0);
document.addEventListener('focus', onfocus);
document.addEventListener('selectionchange', onselch);
timer.add(onfocus);
if (cb_up)
setTimeout(cb_up, 1);
@@ -1503,6 +1501,11 @@ var modal = (function () {
r.hide = function () {
timer.rm(onfocus);
try {
ebi('modal-ok').removeEventListener('blur', onblur);
}
catch (ex) { }
document.removeEventListener('selectionchange', onselch);
document.removeEventListener('focus', onfocus);
document.removeEventListener('keydown', onkey);
o.parentNode.removeChild(o);
@@ -1524,17 +1527,35 @@ var modal = (function () {
cb_ng(null);
}
var onselch = function () {
try {
if (window.getSelection() + '')
r.nofocus = 15;
}
catch (ex) { }
};
var onblur = function () {
r.nofocus = 3;
};
var onfocus = function (e) {
if (MOBILE)
return;
var ctr = ebi('modalc');
if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))
return;
setTimeout(function () {
if (--r.nofocus >= 0)
return;
if (ctr = ebi('modal-ok'))
ctr.focus();
}, 20);
ev(e);
}
};
var onkey = function (e) {
var k = e.code,
@@ -1545,18 +1566,18 @@ var modal = (function () {
if (k == 'Space' && ae && (ae === eok || ae === eng))
k = 'Enter';
if (k == 'Enter') {
if (k.endsWith('Enter')) {
if (ae && ae == eng)
return ng();
return ng(e);
return ok();
return ok(e);
}
if ((k == 'ArrowLeft' || k == 'ArrowRight') && eng && (ae == eok || ae == eng))
return (ae == eok ? eng : eok).focus() || ev(e);
if (k == 'Escape')
return ng();
return ng(e);
}
var next = function () {

View File

@@ -1,3 +1,144 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1208-2133 `v1.9.27` another dedup bug
so [v1.9.26](https://github.com/9001/copyparty/releases/tag/v1.9.26) fixed how moving a symlink could break other related symlinks, and then it turns out symlinks themselves could also die when moving them to another location, and somehow nobody encountered any of these until now... surely there are no more deduplication-related issues left at this point, yeah?
## bugfixes
* #65 moving deduplicated copies of files (symlinks) from one location to another could make them disappear (break the symlinks)
* don't worry, we are **not** talking about data loss! but see the [release notes for v1.9.26](https://github.com/9001/copyparty/releases/tag/v1.9.26) which explain how to deal with this issue (how to find, diagnose, and repair broken symlinks)
----
## regarding fedora packages
[copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) (fedora's build service) is not building at the moment; ***if you installed copyparty from copr-pypi,*** you can upgrade to this release by running one of the following:
```bash
dnf install https://ocv.me/copyparty/fedora/37/python3-copyparty.fc37.noarch.rpm
dnf install https://ocv.me/copyparty/fedora/38/python3-copyparty.fc38.noarch.rpm
dnf install https://ocv.me/copyparty/fedora/39/python3-copyparty.fc39.noarch.rpm
```
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1208-0136 `v1.9.26` dont break symlinks
## new features
* *tumbleweed*
## bugfixes
* deleting files from the server could make some duplicates of that file unavailable (by breaking nested symlinks)
* don't worry, we are **not** talking about data loss! but such broken links would disappear from the directory listing and would need to be remedied by replacing the broken links manually, either by using a file explorer or commandline
* **only** affected linux/macos, did **not** affect servers with `--hardlink` or `--never-symlink` or `--no-dedup`, and **mainly** affected servers with lots of duplicate files (with some dupes in the same folder and some elsewhere)
* if you want to check for such broken symlinks, the following unix command will find all of them: `find -L -type l`
* to repair a broken link, first remove it and then replace it: `rm thelink.opus; ln -s /mnt/music/realfile.opus thelink.opus`
* if you are left with a mystery file and want to know where its duplicates are, you can grep for the filename in the logs and you'll find something like the following line, where the `wark` is the file identifier; grep for that to find all the other copies of that file -- `purl` is the folder/URL which that copy of the file was uploaded to:
```json
{"name": "04. GHOST.opus", "purl": "/mu/vt/suisei/still-still-stellar/", "size": 4520986, "lmod": 1697091772, "sprs": true, "hash": [], "wark": "SJMASMtWOa0UZnc002nn5unO5iCBMa-krt2CDcq8eJe9"}
```
* the server would throw an error if you tried to delete a broken symlink
* prevent warnings about duplicate file entries in the database by preventing that from happening in the first place
* `u2c.py` (commandline uploader) would fail to delete files from the server if there's more than ~10'000 files to be deleted
* and forgot to bump the version number... `1.11 (2nd season)`
## other changes
* `--help` was slightly improved
* docker images are now based on alpine v3.19
* `copyparty.exe` is now based on python v3.11.7
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1201-2326 `v1.9.25` focus
## new features
* remember and restore scroll position when leaving the textfile viewer
## bugfixes
* the request-smuggling detetcor was too strict, blocking access to textfiles with newlines / control-codes in the filename
* focus and text selection in messageboxes was still jank, mainly in firefox and especially phones
## other changes
* the banhammer now applies on attempts at request-smuggling and path traversals
* these were merely detected and rejected before, might as well bonk them
* reject bad requests with a terse 500 instead of abruptly disconnecting in some cases
* stops firefox from rapidly spamming additional attempts
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1201-0210 `v1.9.24` header auth
## new features
* initial work on #62 (support identity providers, oauth/SSO/...); see [readme](https://github.com/9001/copyparty#identity-providers)
* only authentication so far; no authorization yet, and users must exist in the copyparty config with bogus passwords
* new option `--ipa` rejects connections from clients outside of a given allowlist of IP prefixes
* environment variables can be used almost everywhere that takes a filesystem path; should make it way more comfy to write configs for docker / systemd
* #59 added a basic [docker-compose yaml](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose) and an example config
* probably much room for improvement on everything docker still
## bugfixes
* the nftables-based port-forwarding in the [systemd example](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd) was buggy; replaced with CAP_NET_BIND_SERVICE
* palemoon-specific js crash if a text selection was dragged
* text selection in messageboxes was jank
## other changes
* improved [systemd example](https://github.com/9001/copyparty/tree/hovudstraum/contrib/systemd) with hardening and a better example config
* logfiles are flushed for every line written; can be disabled with `--no-logflush` for ~3% more performance best-case
* iphones probably won't broadcast cover-art to car stereos over bluetooth anymore since the thingamajig in iOS that's in charge of that doesn't have cookie-access, and strapping in the auth is too funky so let's stop doing that b7723ac245b8b3e38d6410891ef1aa92d4772114
* can be remedied by enabling filekeys and granting unauthenticated people access that way, but that's too much effort for anyone to bother with I'm sure
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1125-1417 `v1.9.21` in a bind
## new features
* #63 the grid-view will open textfiles in the textfile viewer
* [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh) now accepts user/group names (in addition to IDs)
## bugfixes
* the `Y` hotkey (which turns all links into download links) didn't affect the grid-view
* on some servers with unusual filesystem layouts (especially ubuntu-zfs), [prisonparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/prisonparty.sh) would make an unholy mess of recursive bind-mounts, quickly running out of inodes and requiring a server reboot
* added several safeguards to avoid anything like this in the future
* mutex around jail setup/teardown to prevent racing other instances
* verify jail status by inspecting /proc/mounts between each folder to bind
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1121-2325 `v1.9.20` nice
## new features
* expensive subprocesses (ffmpeg, parsers, hooks) will run with `nice` to reduce cpu priority
* ...so listening to flacs won't grind everything else to a halt
## bugfixes
* the "load more" search results button didn't disappear if you hit the serverside limit
* the "show all" button for huge folders didn't disappear when navigating into a smaller folder
* trying to play the previous track when you're already playing the first track in a folder would send you on a wild adventure
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1119-1229 `v1.9.19` shadow filter
## bugfixes
* #61 Mk.II: filter search results to also handle this issue in volumes where reindexing is disabled, or (spoiler warning:) a bug in the directory indexer prevents shadowed files from being forgotten
* filekeys didn't always get included in the up2k UI for world-readable folders
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-1118-2106 `v1.9.18` cache invalidation

View File

@@ -0,0 +1,33 @@
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # enable multimedia indexing
ansi # enable colors in log messages
# q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker
# ftp: 3921 # enable ftp server on port 3921
# p: 3939 # listen on another port
# ipa: 10.89. # only allow connections from 10.89.*
# df: 16 # stop accepting uploads if less than 16 GB free disk space
# ver # show copyparty version in the controlpanel
# grid # show thumbnails/grid-view by default
# theme: 2 # monokai
# name: datasaver # change the server-name that's displayed in the browser
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
# no-robots, force-js # make it harder for search engines to read your server
[accounts]
ed: wark # username: password
[/] # create a volume at "/" (the webroot), which will
/w # share /w (the docker data volume)
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin

View File

@@ -0,0 +1,20 @@
version: '3'
services:
copyparty:
image: copyparty/ac:latest
container_name: copyparty
user: "1000:1000"
ports:
- 3923:3923
volumes:
- ./:/cfg:z
- /path/to/your/fileshare/top/folder:/w:z
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
healthcheck:
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset"]
interval: 1m
timeout: 2s
retries: 5
start_period: 15s

View File

@@ -0,0 +1,72 @@
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
# example config for how copyparty can be used with an identity
# provider, replacing the built-in authentication/authorization
# mechanism, and instead expecting the reverse-proxy to provide
# the requester's username (and possibly a group-name, for
# optional group-based access control)
#
# the filesystem-path `/w` is used as the storage location
# because that is the data-volume in the docker containers,
# because a deployment like this (with an IdP) is more commonly
# seen in containerized environments -- but this is not required
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # enable multimedia indexing
ansi # enable colors in log messages
# enable IdP support by expecting username/groupname in
# http-headers provided by the reverse-proxy; header "X-IdP-User"
# will contain the username, "X-IdP-Group" the groupname
idp-h-usr: x-idp-user
idp-h-grp: x-idp-group
[/] # create a volume at "/" (the webroot), which will
/w # share /w (the docker data volume)
accs:
rw: * # everyone gets read-access, but
rwmda: %su # the group "su" gets read-write-move-delete-admin
[/u/${u}] # each user gets their own home-folder at /u/username
/w/u/${u} # which will be "u/username" in the docker data volume
accs:
r: * # read-access for anyone, and
rwmda: ${u}, %su # read-write-move-delete-admin for that username + the "su" group
[/u/${u}/priv] # each user also gets a private area at /u/username/priv
/w/u/${u}/priv # stored at DATAVOLUME/u/username/priv
accs:
rwmda: ${u}, %su # read-write-move-delete-admin for that username + the "su" group
[/lounge/${g}] # each group gets their own shared volume
/w/lounge/${g} # stored at DATAVOLUME/lounge/groupname
accs:
r: * # read-access for anyone, and
rwmda: %${g}, %su # read-write-move-delete-admin for that group + the "su" group
[/lounge/${g}/priv] # and a private area for each group too
/w/lounge/${g}/priv # stored at DATAVOLUME/lounge/groupname/priv
accs:
rwmda: %${g}, %su # read-write-move-delete-admin for that group + the "su" group
# and create some strategic volumes to prevent anyone from gaining
# unintended access to priv folders if the users/groups db is lost
[/u]
/w/u
accs:
rwmda: %su
[/lounge]
/w/lounge
accs:
rwmda: %su

View File

@@ -100,6 +100,10 @@ include_trailing_comma = true
[tool.bandit]
skips = ["B104", "B110", "B112"]
[tool.ruff]
line-length = 120
ignore = ["E402", "E722"]
# =====================================================================
[tool.pylint.MAIN]

View File

@@ -17,11 +17,15 @@ docker run --rm -it -u 1000 -p 3923:3923 -v /mnt/nas:/w -v $PWD/cfgdir:/cfg copy
* if you are using rootless podman, remove `-u 1000`
* if you have selinux, append `:z` to all `-v` args (for example `-v /mnt/nas:/w:z`)
i'm unfamiliar with docker-compose and alternatives so let me know if this section could be better 🙏
this example is also available as a podman-compatible [docker-compose yaml](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose); example usage: `docker-compose up` (you may need to `systemctl enable --now podman.socket` or similar)
i'm not very familiar with containers, so let me know if this section could be better 🙏
## configuration
> this section basically explains how the [docker-compose yaml](https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose) works, so you may look there instead
the container has the same default config as the sfx and the pypi module, meaning it will listen on port 3923 and share the "current folder" (`/w` inside the container) as read-write for anyone
the recommended way to configure copyparty inside a container is to mount a folder which has one or more [config files](https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf) inside; `-v /your/config/folder:/cfg`

View File

@@ -29,4 +29,4 @@ ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a250675
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
656015f5cc2c04aa0653ee5609c39a7e5f0b6a58c84fe26b20bd070c52d20b4effb810132f7fb771168483e9fd975cc3302837dd7a1a687ee058b0460c857cc4 packaging-23.2-py3-none-any.whl
6401616fdfdd720d1aaa9a0ed1398d00664b28b6d84517dff8d1f9c416452610c6afa64cfb012a78e61d1cf4f6d0784eca6e7610957859e511f15bc6f3b3bd53 Pillow-10.1.0-cp311-cp311-win_amd64.whl
36442c017d8fc603745d33ca888b5b1194644103cbe1ff53e32d9b0355e290d5efac655fa1ae1b8e552ad8468878dc600d550c1158224260ca463991442e5264 python-3.11.6-amd64.exe
2e6a57bab45b5a825a2073780c73980cbf5aafd99dc3b28660ea3f5f658f04668cd0f01c7de0bb79e362ff4e3b8f01dd4f671d3a2e054d3071baefdcf0b0e4ba python-3.11.7-amd64.exe

View File

@@ -29,7 +29,7 @@ fns=(
mutagen-1.47.0-py3-none-any.whl
packaging-23.2-py3-none-any.whl
Pillow-10.1.0-cp311-cp311-win_amd64.whl
python-3.11.6-amd64.exe
python-3.11.7-amd64.exe
)
[ $w7 ] && fns+=(
pyinstaller-5.13.2-py3-none-win32.whl

View File

@@ -1,6 +1,17 @@
#!/bin/bash
set -ex
# osx support
gtar=$(command -v gtar || command -v gnutar) || true
[ ! -z "$gtar" ] && command -v gfind >/dev/null && {
tar() { $gtar "$@"; }
sed() { gsed "$@"; }
find() { gfind "$@"; }
sort() { gsort "$@"; }
command -v grealpath >/dev/null &&
realpath() { grealpath "$@"; }
}
rm -rf unt
mkdir -p unt/srv
cp -pR copyparty tests unt/
@@ -30,9 +41,11 @@ for py in python{2,3}; do
[ "${1:0:6}" = python ] && [ "$1" != $py ] && continue
PYTHONPATH=
[ $py = python2 ] && PYTHONPATH=../scripts/py2:../sfx/py37
[ $py = python2 ] && PYTHONPATH=../scripts/py2:../sfx/py37:../sfx/j2
export PYTHONPATH
[ $py = python2 ] && py=$(command -v python2.7 || echo $py)
nice $py -m unittest discover -s tests >/dev/null &
pids+=($!)
done

View File

@@ -1,16 +1,16 @@
#!/usr/bin/env python3
import itertools
import re
import sys
import time
import itertools
from . import util as tu
from .util import Cfg
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from . import util as tu
from .util import Cfg
atlas = ["%", "25", "2e", "2f", ".", "/"]

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import sys
import runpy
import sys
host = sys.argv[1]
sys.argv = sys.argv[:1] + sys.argv[2:]

111
tests/test_dots.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import io
import os
import shutil
import tarfile
import tempfile
import unittest
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from copyparty.up2k import Up2k
from copyparty.u2idx import U2idx
from tests import util as tu
from tests.util import Cfg
def hdr(query, uname):
h = "GET /%s HTTP/1.1\r\nPW: %s\r\nConnection: close\r\n\r\n"
return (h % (query, uname)).encode("utf-8")
class TestHttpCli(unittest.TestCase):
def setUp(self):
self.td = tu.get_ramdisk()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def test(self):
td = os.path.join(self.td, "vfs")
os.mkdir(td)
os.chdir(td)
# topDir volA volA/*dirA .volB .volB/*dirB
spaths = " t .t a a/da a/.da .b .b/db .b/.db"
for n, dirpath in enumerate(spaths.split(" ")):
if dirpath:
os.makedirs(dirpath)
for pfx in "f", ".f":
filepath = pfx + str(n)
if dirpath:
filepath = os.path.join(dirpath, filepath)
with open(filepath, "wb") as f:
f.write(filepath.encode("utf-8"))
vcfg = [
".::r,u1:r.,u2",
"a:a:r,u1:r,u2",
".b:.b:r.,u1:r,u2"
]
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], e2dsa=True)
self.asrv = AuthSrv(self.args, self.log)
self.assertEqual(self.tardir("", "u1"), "f0 t/f1 a/f3 a/da/f4")
self.assertEqual(self.tardir(".t", "u1"), "f2")
self.assertEqual(self.tardir(".b", "u1"), ".f6 f6 .db/.f8 .db/f8 db/.f7 db/f7")
zs = ".f0 f0 .t/.f2 .t/f2 t/.f1 t/f1 .b/f6 .b/db/f7 a/f3 a/da/f4"
self.assertEqual(self.tardir("", "u2"), zs)
self.assertEqual(self.curl("?tar", "x")[1][:17], "\nJ2EOT")
# search
up2k = Up2k(self)
u2idx = U2idx(self)
allvols = list(self.asrv.vfs.all_vols.values())
x = u2idx.search("u1", allvols, "", 999)
x = " ".join(sorted([x["rp"] for x in x[0]]))
# u1 can see dotfiles in volB so they should be included
xe = ".b/.db/.f8 .b/.db/f8 .b/.f6 .b/db/.f7 .b/db/f7 .b/f6 a/da/f4 a/f3 f0 t/f1"
self.assertEqual(x, xe)
x = u2idx.search("u2", allvols, "", 999)
x = " ".join(sorted([x["rp"] for x in x[0]]))
self.assertEqual(x, ".f0 .t/.f2 .t/f2 a/da/f4 a/f3 f0 t/.f1 t/f1")
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], dotsrch=False)
self.asrv = AuthSrv(self.args, self.log)
u2idx = U2idx(self)
x = u2idx.search("u1", self.asrv.vfs.all_vols.values(), "", 999)
x = " ".join(sorted([x["rp"] for x in x[0]]))
# u1 can see dotfiles in volB so they should be included
xe = "a/da/f4 a/f3 f0 t/f1"
self.assertEqual(x, xe)
def tardir(self, url, uname):
h, b = self.curl("/" + url + "?tar", uname, True)
tar = tarfile.open(fileobj=io.BytesIO(b), mode="r|").getnames()
top = ("top" if not url else url.lstrip(".").split("/")[0]) + "/"
assert len(tar) == len([x for x in tar if x.startswith(top)])
return " ".join([x[len(top):] for x in tar])
def curl(self, url, uname, binary=False):
conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url, uname))
HttpCli(conn).run()
if binary:
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def log(self, src, msg, c=0):
print(msg)

View File

@@ -4,9 +4,9 @@ from __future__ import print_function, unicode_literals
import re
import unittest
from xml.etree import ElementTree as ET
from copyparty.dxml import parse_xml, BadXML, mkenod, mktnod
from copyparty.dxml import BadXML, mkenod, mktnod, parse_xml
ET.register_namespace("D", "DAV:")

View File

@@ -4,18 +4,17 @@ from __future__ import print_function, unicode_literals
import io
import os
import time
import shutil
import pprint
import shutil
import tarfile
import tempfile
import time
import unittest
from tests import util as tu
from tests.util import Cfg, eprint
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from tests import util as tu
from tests.util import Cfg, eprint
def hdr(query):

View File

@@ -2,19 +2,17 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
import json
import os
import shutil
import tempfile
import unittest
from textwrap import dedent
from copyparty import util
from copyparty.authsrv import VFS, AuthSrv
from tests import util as tu
from tests.util import Cfg
from copyparty.authsrv import AuthSrv, VFS
from copyparty import util
class TestVFS(unittest.TestCase):
def setUp(self):
@@ -176,11 +174,11 @@ class TestVFS(unittest.TestCase):
self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a")
self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertAxs(n.axs.uread, ["*"])
self.assertAxs(n.axs.uread, ["*", "k"])
self.assertAxs(n.axs.uwrite, [])
perm_na = (False, False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False, False)
perm_ro = (True, False, False, False, False, False, False)
perm_na = (False, False, False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False, False, False)
perm_ro = (True, False, False, False, False, False, False, False)
self.assertEqual(vfs.can_access("/", "*"), perm_na)
self.assertEqual(vfs.can_access("/", "k"), perm_rw)
self.assertEqual(vfs.can_access("/a", "*"), perm_ro)
@@ -233,7 +231,7 @@ class TestVFS(unittest.TestCase):
cfg_path = os.path.join(self.td, "test.cfg")
with open(cfg_path, "wb") as f:
f.write(
dedent(
util.dedent(
"""
u a:123
u asd:fgh:jkl

View File

@@ -3,23 +3,23 @@
from __future__ import print_function, unicode_literals
import os
import re
import sys
import time
import shutil
import jinja2
import threading
import tempfile
import platform
import re
import shutil
import subprocess as sp
import sys
import tempfile
import threading
import time
from argparse import Namespace
import jinja2
WINDOWS = platform.system() == "Windows"
ANYWIN = WINDOWS or sys.platform in ["msys"]
MACOS = platform.system() == "Darwin"
J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader)
J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader) # type: ignore
J2_FILES = J2_ENV.from_string("{{ files|join('\n') }}\nJ2EOT")
@@ -43,7 +43,8 @@ if MACOS:
from copyparty.__init__ import E
from copyparty.__main__ import init_E
from copyparty.util import Unrecv, FHC, Garda
from copyparty.util import FHC, Garda, Unrecv
from copyparty.u2idx import U2idx
init_E(E)
@@ -83,8 +84,8 @@ def get_ramdisk():
for _ in range(10):
try:
_, _ = chkcmd(["diskutil", "eraseVolume", "HFS+", "cptd", devname])
with open("/Volumes/cptd/.metadata_never_index", "w") as f:
f.write("orz")
with open("/Volumes/cptd/.metadata_never_index", "wb") as f:
f.write(b"orz")
try:
shutil.rmtree("/Volumes/cptd/.fseventsd")
@@ -99,67 +100,75 @@ def get_ramdisk():
raise Exception("ramdisk creation failed")
ret = os.path.join(tempfile.gettempdir(), "copyparty-test")
try:
if not os.path.isdir(ret):
os.mkdir(ret)
finally:
return subdir(ret)
return subdir(ret)
class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vague_403 vc ver xdev xlink xvol"
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw rand smb srch_dbg stats th_no_crop vague_403 vc ver xdev xlink xvol"
ka.update(**{k: False for k in ex.split()})
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser hist js_browser no_forget no_hash no_idx nonsus_urls"
ex = "ah_cli ah_gen css_browser hist ipa_re js_browser no_forget no_hash no_idx nonsus_urls"
ka.update(**{k: None for k in ex.split()})
ex = "s_thead s_tbody th_convt"
ex = "hash_mt srch_time u2j"
ka.update(**{k: 1 for k in ex.split()})
ex = "reg_cap s_thead s_tbody th_convt"
ka.update(**{k: 9 for k in ex.split()})
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo"
ex = "db_act df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname doctitle favico html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR"
ex = "ah_alg bname doctitle exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR"
ka.update(**{k: "" for k in ex.split()})
ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md"
ex = "exp_lg exp_md th_coversd"
ka.update(**{k: {} for k in ex.split()})
ka.update(ka0)
super(Cfg, self).__init__(
a=a or [],
v=v or [],
c=c,
E=E,
dbd="wal",
s_wr_sz=512 * 1024,
th_size="320x256",
fk_salt="a" * 16,
unpost=600,
u2sort="s",
u2ts="c",
sort="href",
mtp=[],
lang="eng",
log_badpwd=1,
logout=573,
mte={"a": True},
mth={},
lang="eng",
logout=573,
mtp=[],
s_wr_sz=512 * 1024,
sort="href",
srch_hits=99999,
th_size="320x256",
u2sort="s",
u2ts="c",
unpost=600,
warksalt="hunter2",
**ka
)
class NullBroker(object):
def say(*args):
def say(self, *args):
pass
def ask(*args):
def ask(self, *args):
pass
@@ -186,11 +195,16 @@ class VSock(object):
class VHttpSrv(object):
def __init__(self):
def __init__(self, args, asrv, log):
self.args = args
self.asrv = asrv
self.log = log
self.broker = NullBroker()
self.prism = None
self.bans = {}
self.nreq = 0
self.nsus = 0
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"]
self.j2 = {x: J2_FILES for x in aliases}
@@ -200,31 +214,38 @@ class VHttpSrv(object):
self.g403 = Garda("")
self.gurl = Garda("")
self.u2idx = None
self.ptn_cc = re.compile(r"[\x00-\x1f]")
def cachebuster(self):
return "a"
def get_u2idx(self):
self.u2idx = self.u2idx or U2idx(self)
return self.u2idx
class VHttpConn(object):
def __init__(self, args, asrv, log, buf):
self.t0 = time.time()
self.s = VSock(buf)
self.sr = Unrecv(self.s, None)
self.sr = Unrecv(self.s, None) # type: ignore
self.aclose = {}
self.addr = ("127.0.0.1", "42069")
self.args = args
self.asrv = asrv
self.nid = None
self.bans = {}
self.freshen_pwd = 0.0
self.hsrv = VHttpSrv(args, asrv, log)
self.ico = None
self.lf_url = None
self.log_func = log
self.log_src = "a"
self.lf_url = None
self.hsrv = VHttpSrv()
self.bans = {}
self.aclose = {}
self.u2fh = FHC()
self.mutex = threading.Lock()
self.nreq = -1
self.nbyte = 0
self.ico = None
self.nid = None
self.nreq = -1
self.thumbcli = None
self.freshen_pwd = 0.0
self.t0 = time.time()
self.u2fh = FHC()
self.get_u2idx = self.hsrv.get_u2idx