Compare commits

..

No commits in common. "hovudstraum" and "v1.19.3" have entirely different histories.

122 changed files with 1235 additions and 6598 deletions

50
.vscode/launch.json vendored
View file

@ -3,7 +3,7 @@
"configurations": [ "configurations": [
{ {
"name": "Run copyparty", "name": "Run copyparty",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"module": "copyparty", "module": "copyparty",
"console": "integratedTerminal", "console": "integratedTerminal",
@ -11,46 +11,30 @@
"justMyCode": false, "justMyCode": false,
"env": { "env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1", "PYDEVD_DISABLE_FILE_VALIDATION": "1",
"PYTHONWARNINGS": "always" //error "PYTHONWARNINGS": "always", //error
}, },
"args": [ "args": [
//"-nw", // no-write; for testing uploads without writing to disk //"-nw",
//"-q", // quiet; speedboost when console output is not needed "-ed",
"-emp",
// # increase debugger performance:
//"no-htp",
//"hash-mt=0",
//"mtag-mt=1",
//"th-mt=1",
// # listen for FTP and TFTP
"--ftp=3921",
"--ftp-pr=12000-12099",
"--tftp=3969",
// # listen on all IPv6, all IPv4, and unix-socket
"-i::,unix:777:a.sock",
// # misc
"--dedup",
"-e2dsa", "-e2dsa",
"-e2ts", "-e2ts",
"--rss", "-mtp=.bpm=f,bin/mtag/audio-bpm.py",
"--shr=/shr",
"--stats",
"-z",
// # users + volumes
"-aed:wark", "-aed:wark",
"-vdist:dist:r", "-vsrv::r:rw,ed:c,dupe",
"-vsrv::r:rw,ed", "-vdist:dist:r"
"-vsrv/junk:junk:r:A,ed",
"--ver"
] ]
}, },
{
"name": "No debug",
"preLaunchTask": "no_dbg",
"type": "python",
//"request": "attach", "port": 42069
// fork: nc -l 42069 </dev/null
},
{ {
"name": "Run active unit test", "name": "Run active unit test",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"module": "unittest", "module": "unittest",
"console": "integratedTerminal", "console": "integratedTerminal",
@ -67,6 +51,6 @@
"program": "${file}", "program": "${file}",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": false "justMyCode": false
} },
] ]
} }

View file

@ -6,13 +6,13 @@ but please:
# do not use AI / LLM when writing code # do not use AI / LMM when writing code
copyparty is 100% organic, free-range, human-written software! copyparty is 100% organic, free-range, human-written software!
> ⚠ you are now entering a no-copilot zone > ⚠ you are now entering a no-copilot zone
the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate. the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
sorry for the harsh tone, but this is important to me 🙏 sorry for the harsh tone, but this is important to me 🙏

177
README.md
View file

@ -50,7 +50,6 @@ made in Norway 🇳🇴
* [shares](#shares) - share a file or folder by creating a temporary link * [shares](#shares) - share a file or folder by creating a temporary link
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
* [rss feeds](#rss-feeds) - monitor a folder with your RSS reader * [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
* [opds feeds](#opds-feeds) - browse and download files from your e-book reader
* [recent uploads](#recent-uploads) - list all recent uploads * [recent uploads](#recent-uploads) - list all recent uploads
* [media player](#media-player) - plays almost every audio format there is * [media player](#media-player) - plays almost every audio format there is
* [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists * [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists
@ -113,7 +112,6 @@ made in Norway 🇳🇴
* [packages](#packages) - the party might be closer than you think * [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/)) * [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
* [fedora package](#fedora-package) - does not exist yet * [fedora package](#fedora-package) - does not exist yet
* [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`
* [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module) * [nixos module](#nixos-module)
* [browser support](#browser-support) - TLDR: yes * [browser support](#browser-support) - TLDR: yes
@ -138,13 +136,11 @@ made in Norway 🇳🇴
* [dependencies](#dependencies) - mandatory deps * [dependencies](#dependencies) - mandatory deps
* [optional dependencies](#optional-dependencies) - install these to enable bonus features * [optional dependencies](#optional-dependencies) - install these to enable bonus features
* [dependency chickenbits](#dependency-chickenbits) - prevent loading an optional dependency * [dependency chickenbits](#dependency-chickenbits) - prevent loading an optional dependency
* [dependency unvendoring](#dependency-unvendoring) - force use of system modules
* [optional gpl stuff](#optional-gpl-stuff) * [optional gpl stuff](#optional-gpl-stuff)
* [sfx](#sfx) - the self-contained "binary" (recommended!) * [sfx](#sfx) - the self-contained "binary" (recommended!)
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+) * [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+)
* [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) * [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)
* [install on android](#install-on-android) * [install on android](#install-on-android)
* [install on iOS](#install-on-iOS)
* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them * [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them
* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md) * [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
@ -159,7 +155,6 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead * or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
* or install [on arch](#arch-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 are on android, [install copyparty in termux](#install-on-android)
* or maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS)
* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md) * or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)
* or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty` * or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty`
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp) * or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
@ -214,7 +209,6 @@ 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/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/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)
* [contrib/podman-systemd/](contrib/podman-systemd/) to run copyparty in a Podman container as a systemd service (see guide inside)
* [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo * [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo
* [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD * [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD
* [nixos module](#nixos-module) to run copyparty on NixOS hosts * [nixos module](#nixos-module) to run copyparty on NixOS hosts
@ -247,7 +241,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf) * ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)
* ☑ [event hooks](#event-hooks) / script runner * ☑ [event hooks](#event-hooks) / script runner
* ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy) * ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)
* ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64) * ☑ cross-platform (Windows, Linux, Macos, Android, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
* upload * upload
* ☑ basic: plain multipart, ie6 support * ☑ basic: plain multipart, ie6 support
* ☑ [up2k](#uploading): js, resumable, multithreaded * ☑ [up2k](#uploading): js, resumable, multithreaded
@ -270,7 +264,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ play video files as audio (converted on server) * ☑ play video files as audio (converted on server)
* ☑ create and play [m3u8 playlists](#playlists) * ☑ create and play [m3u8 playlists](#playlists)
* ☑ image gallery with webm player * ☑ image gallery with webm player
* ☑ [textfile browser](#textfile-viewer) with syntax highlighting * ☑ [textfile browser](#textfile-viewer) with syntax hilighting
* ☑ realtime streaming of growing files (logfiles and such) * ☑ realtime streaming of growing files (logfiles and such)
* ☑ [thumbnails](#thumbnails) * ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow, pyvips, or FFmpeg * ☑ ...of images using Pillow, pyvips, or FFmpeg
@ -575,8 +569,6 @@ for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mn
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
> this also works for single files, because files can also be volumes
## dotfiles ## dotfiles
@ -731,10 +723,6 @@ to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` an
* the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types) * the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)
* be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however * be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however
note:
* heif/heifs/heic/heics images usually require the `libvips` [optional dependency](#optional-dependencies) (available in the `iv` docker image, `withFastThumbnails` in nixos)
* technical trivia: FFmpeg has basic support for tiled heic as of v7.0; need `-show_stream_groups` for correct resolution
config file example: config file example:
```yaml ```yaml
@ -843,7 +831,7 @@ the up2k UI is the epitome of polished intuitive experiences:
* `[🔎]` switch between upload and [file-search](#file-search) mode * `[🔎]` switch between upload and [file-search](#file-search) mode
* ignore `[🔎]` if you add files by dragging them into the browser * ignore `[🔎]` if you add files by dragging them into the browser
and then there's the tabs below it, and then theres the tabs below it,
* `[ok]` is the files which completed successfully * `[ok]` is the files which completed successfully
* `[ng]` is the ones that failed / got rejected (already exists, ...) * `[ng]` is the ones that failed / got rejected (already exists, ...)
* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order * `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order
@ -875,8 +863,6 @@ the files will be hashed on the client-side, and each hash is sent to the server
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]` files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much * the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
if you have a "wark" (file-identifier/checksum) then you can also search for that in the [🔎] tab by putting `w = kFpDiztbZc8Z1Lzi` in the `raw` field
### unpost ### unpost
@ -1003,8 +989,6 @@ available functions:
* `$lpad(text, length, pad_char)` * `$lpad(text, length, pad_char)`
* `$rpad(text, length, pad_char)` * `$rpad(text, length, pad_char)`
two counters are available; `.n.s` is the nth file in the selection, and `.n.d` the nth file in the folder, for example rename-output `file(.n.d).(ext)` gives `file5.bin`, and `beach-$lpad((.n.s),3,0).(ext)` is `beach-017.jpg` and the initial value of each counter can be set in the textboxes underneath the preset dropdown
so, so,
say you have a file named [`meganeko - Eclipse - 07 Sirius A.mp3`](https://www.youtube.com/watch?v=-dtb0vDPruI) (absolutely fantastic album btw) and the tags are: `Album:Eclipse`, `Artist:meganeko`, `Title:Sirius A`, `tn:7` say you have a file named [`meganeko - Eclipse - 07 Sirius A.mp3`](https://www.youtube.com/watch?v=-dtb0vDPruI) (absolutely fantastic album btw) and the tags are: `Album:Eclipse`, `Artist:meganeko`, `Title:Sirius A`, `tn:7`
@ -1040,8 +1024,6 @@ url parameters:
* `pw=hunter2` for password auth * `pw=hunter2` for password auth
* if you enabled `--usernames` then do `pw=username:password` instead * if you enabled `--usernames` then do `pw=username:password` instead
* `nopw` disables embedding the password (if provided) into item-URLs in the feed
* `nopw=a` disables mentioning the password anywhere at all in the feed; may break some readers
* `recursive` to also include subfolders * `recursive` to also include subfolders
* `title=foo` changes the feed title (default: folder name) * `title=foo` changes the feed title (default: folder name)
* `fext=mp3,opus` only include mp3 and opus files (default: all) * `fext=mp3,opus` only include mp3 and opus files (default: all)
@ -1053,28 +1035,6 @@ url parameters:
* uppercase = reverse-sort; `M` = oldest file first * uppercase = reverse-sort; `M` = oldest file first
## opds feeds
browse and download files from your e-book reader
enabled with the `opds` volflag or `--opds` global option
add `?opds` to the end of the url you would like to browse, then input that in your opds client.
for example: `https://copyparty.example/books/?opds`.
to log in with a password, enter it into either of the username or password fields in your client.
- if you've enabled `--usernames`, then you need to enter both username and password .
note: some clients (e.g. Moon+ Reader) will not send the password when downloading cover images, which will
cause your ip to be banned by copyparty. to work around this, you can grant the [`g` permission](#accounts-and-volumes)
to unauthenticated requests and enable [filekeys](#filekeys) to prevent guessing filenames. for example:
`-vbooks:books:r,ed:g:c,fk,opds`
by default, not all file types will be listed in opds feeds. to change this, add the extension to
`--opds-exts` (volflag: `opds_exts`), or empty the list to list everything
## recent uploads ## recent uploads
list all recent uploads by clicking "show recent uploads" in the controlpanel list all recent uploads by clicking "show recent uploads" in the controlpanel
@ -1103,12 +1063,11 @@ plays almost every audio format there is (if the server has FFmpeg installed fo
the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav` the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav`
some highlights: some hilights:
* OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png)) * OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png))
* shows the audio waveform in the seekbar * shows the audio waveform in the seekbar
* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended * not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended
* videos can be played as audio, without wasting bandwidth on the video * videos can be played as audio, without wasting bandwidth on the video
* adding `?v` to the end of an audio/video/image link will make it open in the mediaplayer
click the `play` link next to an audio file, or copy the link target to [share it](https://a.ocv.me/pub/demo/music/Ubiktune%20-%20SOUNDSHOCK%202%20-%20FM%20FUNK%20TERRROR!!/#af-1fbfba61&t=18) (optionally with a timestamp to start playing from, like that example does) click the `play` link next to an audio file, or copy the link target to [share it](https://a.ocv.me/pub/demo/music/Ubiktune%20-%20SOUNDSHOCK%202%20-%20FM%20FUNK%20TERRROR!!/#af-1fbfba61&t=18) (optionally with a timestamp to start playing from, like that example does)
@ -1335,12 +1294,6 @@ print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/
* `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2` * `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2`
* `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size * `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size
* 1x may render incorrectly on some terminals/fonts, but 2x should always work * 1x may render incorrectly on some terminals/fonts, but 2x should always work
* `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away)
* `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt`
* `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt`
* `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg`
* `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png`
* `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png`
it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain
@ -1364,16 +1317,6 @@ some recommended FTP / FTPS clients; `wark` = example password:
* https://rclone.org/ does FTPS with `tls=false explicit_tls=true` * https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
* `lftp -u k,wark -p 3921 127.0.0.1 -e ls` * `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
* `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'` * `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
* `curl ftp://127.0.0.1:3921/` (plaintext ftp)
* `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)
config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:
```yaml
[global]
ftp: 3921
ftp-pr: 12000-12099
```
## webdav server ## webdav server
@ -1389,7 +1332,7 @@ general usage:
on macos, connect from finder: on macos, connect from finder:
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/ * [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
to upload or edit files with WebDAV clients, enable the `daw` volflag (because most WebDAV clients expect this) and give your account the delete-permission. This avoids getting several copies of the same file on the server. HOWEVER: This will also make all PUT-uploads overwrite existing files if the user has delete-access, so use with caution. in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
> note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients) > note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients)
@ -1469,7 +1412,6 @@ and some minor issues,
* win10 onwards does not allow connecting anonymously / without accounts * win10 onwards does not allow connecting anonymously / without accounts
* python3 only * python3 only
* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster) * slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
* those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav
known client bugs: known client bugs:
* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2 * on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
@ -1590,7 +1532,6 @@ the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`,
note: note:
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings) * upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
* and file checksums can be shown with global-option `-e2d -mte +w` or volflag `e2d,mte=+w` (always active for users with permission `a`)
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise * `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
config file example (these options are recommended btw): config file example (these options are recommended btw):
@ -1674,7 +1615,6 @@ set upload rules using volflags, some examples:
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`: * `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1) * `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format * `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
* `:c,rotf_tz=Europe/Oslo` sets the timezone (default is UTC unless global-option `rotf-tz` is changed)
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example * if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string * but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
* just to avoid additional complexity in up2k which is enough of a mess already * just to avoid additional complexity in up2k which is enough of a mess already
@ -1980,10 +1920,6 @@ you can disable the built-in password-based login system, and instead replace it
* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
* `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password
* the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login
* if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party * if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
@ -2224,7 +2160,7 @@ when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-f
in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet) in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet)
* if these results are bullshit because my config examples are bad, please submit corrections! * if these results are bullshit because my config exampels are bad, please submit corrections!
## permanent cloudflare tunnel ## permanent cloudflare tunnel
@ -2360,7 +2296,6 @@ buggy feature? rip it out by setting any of the following environment variables
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) | | `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy | | `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider | | `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
| `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations |
example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py` example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
@ -2396,15 +2331,6 @@ after installing, start either the system service or the user service and naviga
does not exist yet; there are rumours that it is being packaged! keep an eye on this space... does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
## homebrew formulae
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
should work on all macs (both intel and apple silicon) and all relevant macos versions
the homebrew package is maintained by the homebrew team (thanks!)
## nix package ## nix package
`nix profile install github:9001/copyparty` `nix profile install github:9001/copyparty`
@ -2418,7 +2344,7 @@ some recommended dependencies are enabled by default; [override the package](htt
## nixos module ## nixos module
for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS: for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of NixOS.
```nix ```nix
{ {
@ -2445,41 +2371,10 @@ for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS:
} }
``` ```
if you don't use a flake in your configuration, you can use other dependency management tools like [npins](https://github.com/andir/npins), [niv](https://github.com/nmattia/niv), or even plain [`fetchTarball`](https://nix.dev/manual/nix/stable/language/builtins#builtins-fetchTarball), like so:
```nix
{ pkgs, ... }:
let
# npins example, adjust for your setup. copyparty should be a path to the downloaded repo
# for niv, just replace the npins folder import with the sources.nix file
copyparty = (import ./npins).copyparty;
# or with fetchTarball:
copyparty = fetchTarball "https://github.com/9001/copyparty/archive/hovudstraum.tar.gz";
in
{
# load the copyparty NixOS module
imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ];
# add the copyparty overlay to expose the package to the module
nixpkgs.overlays = [ (import "${copyparty}/contrib/package/nix/overlay.nix") ];
# (optional) install the package globally
environment.systemPackages = [ pkgs.copyparty ];
# configure the copyparty module
services.copyparty.enable = true;
}
```
copyparty on NixOS is configured via `services.copyparty` options, for example: copyparty on NixOS is configured via `services.copyparty` options, for example:
```nix ```nix
services.copyparty = { services.copyparty = {
enable = true; enable = true;
# the user to run the service as
user = "copyparty";
# the group to run the service as
group = "copyparty";
# directly maps to values in the [global] section of the copyparty config. # directly maps to values in the [global] section of the copyparty config.
# see `copyparty --help` for available options # see `copyparty --help` for available options
settings = { settings = {
@ -2504,12 +2399,6 @@ services.copyparty = {
k.passwordFile = "/run/keys/copyparty/k_password"; k.passwordFile = "/run/keys/copyparty/k_password";
}; };
# create a group
groups = {
# users "ed" and "k" are part of the group g1
g1 = [ "ed" "k" ];
};
# create a volume # create a volume
volumes = { volumes = {
# create a volume at "/" (the webroot), which will # create a volume at "/" (the webroot), which will
@ -2668,20 +2557,11 @@ sync folders to/from copyparty
NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty
* if you want bidirectional sync, then copyparty and syncthing *should* be entirely safe to combine; they should be able to collaborate on the same folders without causing any trouble for eachother. Many people do this, and there have been no issues so far. But, if you *do* encounter any problems, please [file a copyparty bug](https://github.com/9001/copyparty/issues/new/choose) and I'll try to help -- just keep in mind I've never used syncthing before :-)
the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading) the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading)
if you want to sync with `u2c.py` then:
* the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into
* ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into
* ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a`
* quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync
alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare
* starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections * starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections
* but this is only true for the initial upload; u2c will be faster for periodic syncing
## mount as drive ## mount as drive
@ -2723,8 +2603,6 @@ there is no iPhone app, but the following shortcuts are almost as good:
* can download links and rehost the target file on copyparty (see first comment inside the shortcut) * can download links and rehost the target file on copyparty (see first comment inside the shortcut)
* pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there * pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there
if you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS)
# performance # performance
@ -2753,10 +2631,6 @@ below are some tweaks roughly ordered by usefulness:
* using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others * using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others
* and pypy can sometimes crash on startup with `-j0` (TODO make issue) * and pypy can sometimes crash on startup with `-j0` (TODO make issue)
* if you are running the copyparty server **on Windows or Macos:**
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* this is the same as `casechk: n` in a config-file
## client-side ## client-side
@ -2999,20 +2873,6 @@ example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
* python2.7 on windows: `PRTY_NO_FFMPEG` + `PRTY_NO_FFPROBE` saves startup time * python2.7 on windows: `PRTY_NO_FFMPEG` + `PRTY_NO_FFPROBE` saves startup time
### dependency unvendoring
force use of system modules instead of the vendored versions:
| env-var | what it does |
| -------------------- | ------------ |
| `PRTY_SYS_ALL` | all of the below |
| `PRTY_SYS_DNSLIB` | replace [stolen/dnslib](./copyparty/stolen/dnslib) with [upstream](https://pypi.org/project/dnslib/) |
| `PRTY_SYS_IFADDR` | replace [stolen/ifaddr](./copyparty/stolen/ifaddr) with [upstream](https://pypi.org/project/ifaddr/) |
| `PRTY_SYS_QRCG` | replace [stolen/qrcodegen.py](./copyparty/stolen/qrcodegen.py) with [upstream](https://github.com/nayuki/QR-Code-generator/blob/master/python/qrcodegen.py) |
to debug, run copyparty with `PRTY_MODSPEC=1` to see where it's getting each module from
## optional gpl stuff ## optional gpl stuff
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag) some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
@ -3075,29 +2935,6 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132
* or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})` * or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`
if you are suddenly unable to access storage (permission issues), try forcequitting termux, revoke all of its permissions in android settings, and run the command `termux-setup-storage`
# install on iOS
first install one of the following:
* [a-Shell mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) gives you the essential features
* [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) also enables audio transcoding and better thubmnails
and then copypaste the following command into `a-Shell`:
```sh
curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
```
what this does:
* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc`
* adds the command `cpp` to launch copyparty with that config file
known issues:
* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads
* the best way to exit copyparty is to swipe away the app
# reporting bugs # reporting bugs

View file

@ -1,7 +1,7 @@
# [`u2c.py`](u2c.py) # [`u2c.py`](u2c.py)
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm) * command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* file uploads, file-search, autoresume of aborted/broken uploads * file uploads, file-search, autoresume of aborted/broken uploads
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync) * sync local folder to server
* generally faster than browsers * generally faster than browsers
* if something breaks just restart it * if something breaks just restart it

View file

@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
# usage: ./bubbleparty.sh ./copyparty-sfx.py .... # usage: ./bubbleparty.sh ./copyparty-sfx.py ....
bwrap \ bwrap \
--unshare-all \ --unshare-all \
@ -9,7 +9,7 @@ bwrap \
--dev-bind /dev /dev \ --dev-bind /dev /dev \
--dir /tmp \ --dir /tmp \
--dir /var \ --dir /var \
--bind "$(pwd)" "$(pwd)" \ --bind $(pwd) $(pwd) \
--share-net \ --share-net \
--die-with-parent \ --die-with-parent \
--file 11 /etc/passwd \ --file 11 /etc/passwd \

View file

@ -8,7 +8,7 @@ import sqlite3
import argparse import argparse
DB_VER1 = 3 DB_VER1 = 3
DB_VER2 = 6 DB_VER2 = 5
BY_PATH = None BY_PATH = None
NC = None NC = None
@ -39,7 +39,7 @@ def ls(db):
print(f"{nfiles} files") print(f"{nfiles} files")
print(f"{ntags} tags\n") print(f"{ntags} tags\n")
print("number of occurrences for each tag,") print("number of occurences for each tag,")
print(" 'x' = file has no tags") print(" 'x' = file has no tags")
print(" 't:mtp' = the mtp flag (file not mtp processed yet)") print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
print() print()

View file

@ -28,19 +28,10 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions * [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension * [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) * good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
# on message # on message
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty * [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
* [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url * [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty * [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder * [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
# general concept demos
* [import-me.py](import-me.py) shows how the `I` flag makes the hook 140x faster (but you need to be Very Careful when writing the plugin)
* [wget-i.py](wget-i.py) is an import-safe modification of [wget.py](wget.py)

View file

@ -1,55 +0,0 @@
#!/usr/bin/env python3
from typing import Any
_ = r"""
the fastest hook in the west
(runs directly inside copyparty, not as a subprocess)
example usage as global config:
--xbu I,bin/hooks/import-me.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: I,bin/hooks/import-me.py
parameters explained,
I = import; do not fork / subprocess
IMPORTANT NOTE:
because this hook is running inside copyparty, you need to
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
DO NOT os.chdir() or anything like that, and also make sure
that the name of this file is unique (cannot be the same as
an existing python module/library)
"""
def main(ka: dict[str, Any]) -> dict[str, Any]:
# "ka" is a dictionary with info from copyparty...
# but because we are running inside copyparty, we don't need such courtesies;
import inspect
cf = inspect.currentframe().f_back.f_back.f_back
t = "hello from hook; I am able to peek into copyparty's memory like so:\n function name: %s\n variables:\n %s\n"
t2 = "\n ".join([("%r: %r" % (k, v))[:99] for k, v in cf.f_locals.items()][:9])
logger = ka["log"]
logger(t % (cf.f_code, t2))
# must return a dictionary with:
# "rc": the retcode; 0 is ok
return {"rc": 0}

View file

@ -9,7 +9,7 @@ from plyer import notification
_ = r""" _ = r"""
show os notification on upload; works on windows, linux, macos, android show os notification on upload; works on windows, linux, macos, android
dependencies: depdencies:
windows: python3 -m pip install --user -U plyer windows: python3 -m pip install --user -U plyer
linux: python3 -m pip install --user -U plyer linux: python3 -m pip install --user -U plyer
macos: python3 -m pip install --user -U plyer pyobjus macos: python3 -m pip install --user -U plyer pyobjus

View file

@ -1,60 +0,0 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
_ = r"""
reject file upload (with a nice explanation why)
example usage as global config:
--xbu j,c1,bin/hooks/reject-and-explain.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: j,c1,bin/hooks/reject-and-explain.py
parameters explained,
xbu = execute-before-upload (can also be xau, execute-after-upload)
j = this hook needs upload information as json (not just the filename)
c1 = this hook returns json on stdout, so tell copyparty to read that
"""
def main():
inf = json.loads(sys.argv[1])
vdir, fn = os.path.split(inf["vp"])
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
# the following is what decides if we'll accept the upload or reject it:
# we check if the upload-folder url matches the following regex-pattern:
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
if ok:
# allow the upload
print("{}")
return
# the upload was rejected; display the following errortext:
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
errmsg += inf["vp"] # if you want to mention the file's url
print(json.dumps({"rejectmsg": errmsg}))
if __name__ == "__main__":
main()

View file

@ -1,72 +0,0 @@
#!/usr/bin/env python3
import os
import threading
from argparse import Namespace
from jinja2.nodes import Name
from copyparty.fsutil import Fstab
from typing import Any, Optional
_ = r"""
reject an upload if the target folder is on a ramdisk; useful when you
have a volume where some folders inside are ramdisks but others aren't
example usage as global config:
--xbu I,bin/hooks/reject-ramdisk.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/reject-ramdisk.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: I,bin/hooks/reject-ramdisk.py
parameters explained,
I = import; do not fork / subprocess
IMPORTANT NOTE:
because this hook is imported inside copyparty, you need to
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
DO NOT os.chdir() or anything like that, and also make sure
that the name of this file is unique (cannot be the same as
an existing python module/library)
"""
mutex = threading.Lock()
fstab: Optional[Fstab] = None
def main(ka: dict[str, Any]) -> dict[str, Any]:
global fstab
with mutex:
log = ka["log"] # this is a copyparty NamedLogger function
if not fstab:
log("<HOOK:RAMDISK> creating fstab", 6)
args = Namespace()
args.mtab_age = 1 # cache the filesystem info for 1 sec
fstab = Fstab(log, args, False)
ap = ka["ap"] # abspath the upload is going to
fs, mp = fstab.get(ap) # figure out what the filesystem is
ramdisk = fs in ("tmpfs", "overlay") # looks like a ramdisk?
# log("<HOOK:RAMDISK> fs=%r" % (fs,))
if ramdisk:
t = "Upload REJECTED because destination is a ramdisk"
return {"rc": 1, "rejectmsg": t}
return {"rc": 0}

View file

@ -1,97 +0,0 @@
#!/usr/bin/env python3
import os
import threading
import subprocess as sp
_ = r"""
use copyparty as a file downloader by POSTing URLs as
application/x-www-form-urlencoded (for example using the
📟 message-to-server-log in the web-ui)
this hook is a modified copy of wget.py, modified to
make it import-safe so it can be run with the 'I' flag,
which speeds up the startup time of the hook by 140x
example usage as global config:
--xm aw,I,bin/hooks/wget-i.py
parameters explained,
xm = execute on message-to-server-log
aw = only users with write-access can use this
I = import; do not fork / subprocess
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/wget.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all messages with the params explained above)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xm: aw,I,bin/hooks/wget.py
the volflag examples only kicks in if you send the message
while you're in the /inc folder (or any folder below there)
IMPORTANT NOTE:
because this hook uses the 'I' flag to run inside copyparty,
many other flags will not work (f,j,c3,t3600 as seen in the
original wget.py), and furthermore + more importantly we
need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so
the os.chdir has been replaced with cwd=dirpath for example
"""
def do_stuff(inf):
"""
worker function which is executed in another thread to
avoid blocking copyparty while the download is running,
since we cannot use the 'f,t3600' hook-flags with 'I'
"""
# first things first; grab the logger-function which copyparty is letting us borrow
log = inf["log"]
url = inf["txt"]
if "://" not in url:
url = "https://" + url
proto = url.split("://")[0].lower()
if proto not in ("http", "https", "ftp", "ftps"):
raise Exception("bad proto {}".format(proto))
dirpath = inf["ap"]
name = url.split("?")[0].split("/")[-1]
msg = "-- DOWNLOADING " + name
log(msg)
tfn = os.path.join(dirpath, msg)
open(tfn, "wb").close()
cmd = ["wget", "--trust-server-names", "-nv", "--", url]
try:
# two things to note here:
# - cannot use the `c3` hook-flag with `I` so mute output with stdout=sp.DEVNULL instead;
# - MUST NOT use os.chdir with 'I' so use cwd=dirpath instead
sp.check_call(cmd, cwd=dirpath, stdout=sp.DEVNULL)
except:
t = "-- FAILED TO DOWNLOAD " + name
log(t, 3) # 3=yellow=warning
open(os.path.join(dirpath, t), "wb").close()
raise # have copyparty scream about the details in the log
os.unlink(tfn)
def main(inf):
threading.Thread(target=do_stuff, args=(inf,), daemon=True).start()

View file

@ -66,7 +66,7 @@ def main():
try: try:
sp.check_call(cmd) sp.check_call(cmd)
except: except:
t = "-- FAILED TO DOWNLOAD " + name t = "-- FAILED TO DONWLOAD " + name
print(f"{t}\n", end="") print(f"{t}\n", end="")
open(t, "wb").close() open(t, "wb").close()

View file

@ -7,7 +7,7 @@ example copyparty config to use this:
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
explained: explained:
for realpath srv/hello (served at /hello), write-only for everyone, for realpath srv/hello (served at /hello), write-only for eveyrone,
enable file analysis on upload (e2ts), enable file analysis on upload (e2ts),
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook", use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
do this on all uploads regardless of extension, do this on all uploads regardless of extension,

View file

@ -11,7 +11,7 @@ example copyparty config to use this:
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
explained: explained:
for realpath srv/hello (served at /hello),write-only for everyone, for realpath srv/hello (served at /hello),write-only for eveyrone,
enable file analysis on upload (e2ts), enable file analysis on upload (e2ts),
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb", use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
do this on all uploads with the file extension "bin", do this on all uploads with the file extension "bin",

View file

@ -84,7 +84,7 @@ def main():
# on success, delete the .bin file which contains the URL # on success, delete the .bin file which contains the URL
os.unlink(fp) os.unlink(fp)
except: except:
open("-- FAILED TO DOWNLOAD " + name, "wb").close() open("-- FAILED TO DONWLOAD " + name, "wb").close()
os.unlink(tfn) os.unlink(tfn)
print(url) print(url)

View file

@ -6,8 +6,8 @@ __copyright__ = 2019
__license__ = "MIT" __license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/" __url__ = "https://github.com/9001/copyparty/"
S_VERSION = "2.1" S_VERSION = "2.0"
S_BUILD_DT = "2025-09-06" S_BUILD_DT = "2024-10-01"
""" """
mount a copyparty server (local or remote) as a filesystem mount a copyparty server (local or remote) as a filesystem
@ -99,7 +99,7 @@ except:
elif MACOS: elif MACOS:
libfuse = "install https://osxfuse.github.io/" libfuse = "install https://osxfuse.github.io/"
else: else:
libfuse = "apt install libfuse2\n modprobe fuse" libfuse = "apt install libfuse3-3\n modprobe fuse"
m = """\033[33m m = """\033[33m
could not import fuse; these may help: could not import fuse; these may help:
@ -359,7 +359,7 @@ class Gateway(object):
def sendreq(self, meth, path, headers, **kwargs): def sendreq(self, meth, path, headers, **kwargs):
tid = get_tid() tid = get_tid()
if self.password: if self.password:
headers["PW"] = self.password headers["Cookie"] = "=".join(["cppwd", self.password])
try: try:
c = self.getconn(tid) c = self.getconn(tid)
@ -902,7 +902,9 @@ class CPPF(Operations):
return ret return ret
def _readdir(self, path, fh=None): def _readdir(self, path, fh=None):
dbg("dircache miss") path = path.strip("/")
dbg("readdir %r [%s]", path, fh)
ret = self.gw.listdir(path) ret = self.gw.listdir(path)
if not self.n_dircache: if not self.n_dircache:
return ret return ret
@ -912,17 +914,11 @@ class CPPF(Operations):
self.dircache.append(cn) self.dircache.append(cn)
self.clean_dircache() self.clean_dircache()
# import pprint; pprint.pprint(ret)
return ret return ret
def readdir(self, path, fh=None): def readdir(self, path, fh=None):
dbg("readdir %r [%s]", path, fh) return [".", ".."] + list(self._readdir(path, fh))
path = path.strip("/")
cn = self.get_cached_dir(path)
if cn:
ret = cn.data
else:
ret = self._readdir(path, fh)
return [".", ".."] + list(ret)
def read(self, path, length, offset, fh=None): def read(self, path, length, offset, fh=None):
req_max = 1024 * 1024 * 8 req_max = 1024 * 1024 * 8
@ -997,6 +993,7 @@ class CPPF(Operations):
if cn: if cn:
dents = cn.data dents = cn.data
else: else:
dbg("cache miss")
dents = self._readdir(dirpath) dents = self._readdir(dirpath)
try: try:
@ -1144,15 +1141,10 @@ def main():
if WINDOWS: if WINDOWS:
examples.append("http://192.168.1.69:3923/music/ M:") examples.append("http://192.168.1.69:3923/music/ M:")
epi = "example:" + ex_pre + ex_pre.join(examples)
epi += """\n
NOTE: if server has --usernames enabled, then password is "username:password"
"""
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
formatter_class=TheArgparseFormatter, formatter_class=TheArgparseFormatter,
description="mount a copyparty server as a local filesystem -- " + ver, description="mount a copyparty server as a local filesystem -- " + ver,
epilog=epi, epilog="example:" + ex_pre + ex_pre.join(examples),
) )
# fmt: off # fmt: off
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount") ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")

View file

@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
# create a dev # create a dev
(cd "$jail"; mkdir -p dev; cd dev (cd $jail; mkdir -p dev; cd dev
[ -e null ] || mknod -m 666 null c 1 3 [ -e null ] || mknod -m 666 null c 1 3
[ -e zero ] || mknod -m 666 zero c 1 5 [ -e zero ] || mknod -m 666 zero c 1 5
[ -e random ] || mknod -m 444 random c 1 8 [ -e random ] || mknod -m 444 random c 1 8

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
S_VERSION = "2.13" S_VERSION = "2.11"
S_BUILD_DT = "2025-09-05" S_BUILD_DT = "2025-05-18"
""" """
u2c.py: upload to copyparty u2c.py: upload to copyparty
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
- dependencies: no - dependencies: no
- supports python 2.6, 2.7, and 3.3 through 3.14 - supports python 2.6, 2.7, and 3.3 through 3.12
- if something breaks just try again and it'll autoresume - if something breaks just try again and it'll autoresume
""" """
@ -590,10 +590,9 @@ def undns(url):
def _scd(err, top): def _scd(err, top):
"""non-recursive listing of directory contents, along with stat() info""" """non-recursive listing of directory contents, along with stat() info"""
top_ = os.path.join(top, b"")
with os.scandir(top) as dh: with os.scandir(top) as dh:
for fh in dh: for fh in dh:
abspath = top_ + fh.name abspath = os.path.join(top, fh.name)
try: try:
yield [abspath, fh.stat()] yield [abspath, fh.stat()]
except Exception as ex: except Exception as ex:
@ -602,9 +601,8 @@ def _scd(err, top):
def _lsd(err, top): def _lsd(err, top):
"""non-recursive listing of directory contents, along with stat() info""" """non-recursive listing of directory contents, along with stat() info"""
top_ = os.path.join(top, b"")
for name in os.listdir(top): for name in os.listdir(top):
abspath = top_ + name abspath = os.path.join(top, name)
try: try:
yield [abspath, os.stat(abspath)] yield [abspath, os.stat(abspath)]
except Exception as ex: except Exception as ex:
@ -679,7 +677,7 @@ def walkdirs(err, tops, excl):
yield stop, ap[len(stop) :].lstrip(sep), inf yield stop, ap[len(stop) :].lstrip(sep), inf
else: else:
d, n = top.rsplit(sep, 1) d, n = top.rsplit(sep, 1)
yield d or b"/", n, os.stat(top) yield d, n, os.stat(top)
# mostly from copyparty/util.py # mostly from copyparty/util.py
@ -1529,10 +1527,10 @@ def main():
# fmt: off # fmt: off
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog=""" ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
NOTE: source file/folder selection uses rsync syntax, meaning that: NOTE:
source file/folder selection uses rsync syntax, meaning that:
"foo" uploads the entire folder to URL/foo/ "foo" uploads the entire folder to URL/foo/
"foo/" uploads the CONTENTS of the folder into URL/ "foo/" uploads the CONTENTS of the folder into URL/
NOTE: if server has --usernames enabled, then password is "username:password"
""") """)
ap.add_argument("url", type=unicode, help="server url, including destination folder") ap.add_argument("url", type=unicode, help="server url, including destination folder")

View file

@ -7,12 +7,6 @@
* works on windows, linux and macos * works on windows, linux and macos
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat` * assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
### [`setup-ashell.sh`](setup-ashell.sh)
* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
* not very useful due to limitations in iOS:
* not able to share all of your phone's storage
* cannot run in the background
### [`index.html`](index.html) ### [`index.html`](index.html)
* drop-in redirect from an httpd to copyparty * drop-in redirect from an httpd to copyparty
* assumes the webserver and copyparty is running on the same server/IP * assumes the webserver and copyparty is running on the same server/IP

View file

@ -31,7 +31,7 @@
# generate the list of permitted IP ranges like so: # generate the list of permitted IP ranges like so:
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf # (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
# #
# and then enable it below by uncommenting the cloudflare-only.conf line # and then enable it below by uncomenting the cloudflare-only.conf line
# #
# ====================================================================== # ======================================================================

View file

@ -50,9 +50,7 @@ let
configStr = '' configStr = ''
${mkSection "global" cfg.settings} ${mkSection "global" cfg.settings}
${cfg.globalExtraConfig}
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)} ${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
${mkSection "groups" cfg.groups}
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)} ${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
''; '';
@ -133,12 +131,6 @@ in
''; '';
}; };
globalExtraConfig = mkOption {
type = types.str;
default = "";
description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set.";
};
accounts = mkOption { accounts = mkOption {
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
@ -168,19 +160,6 @@ in
''; '';
}; };
groups = mkOption {
type = types.attrsOf (types.listOf types.str);
description = ''
A set of copyparty groups to create and the users that should be part of each group.
'';
default = { };
example = literalExpression ''
{
group_name = [ "user1" "user2" ];
};
'';
};
volumes = mkOption { volumes = mkOption {
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
@ -370,17 +349,13 @@ in
) cfg.volumes ) cfg.volumes
); );
users.groups = lib.mkIf (cfg.group == "copyparty") { users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
copyparty = { }; users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
};
users.users = lib.mkIf (cfg.user == "copyparty") {
copyparty = {
description = "Service user for copyparty"; description = "Service user for copyparty";
group = cfg.group; group = "copyparty";
home = externalStateDir; home = externalStateDir;
isSystemUser = true; isSystemUser = true;
}; };
};
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [ environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
(pkgs.writeShellScriptBin "copyparty-hash" '' (pkgs.writeShellScriptBin "copyparty-hash" ''
set -a # automatically export variables set -a # automatically export variables

View file

@ -3,7 +3,7 @@
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty pkgname=copyparty
pkgver="1.19.16" pkgver="1.19.2"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@ -23,7 +23,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") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}/copyparty.conf" ) backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("d8cc10d3623eaccc8acaebdd3b0102a1ebf878a03ffe6e737d132c66f799d682") sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -2,7 +2,7 @@
pkgname=copyparty pkgname=copyparty
pkgver=1.19.16 pkgver=1.19.2
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@ -20,7 +20,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") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("/etc/${pkgname}.d/init" ) backup=("/etc/${pkgname}.d/init" )
sha256sums=("d8cc10d3623eaccc8acaebdd3b0102a1ebf878a03ffe6e737d132c66f799d682") sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -26,7 +26,7 @@ Environment=XDG_CONFIG_HOME=/home/cpp/.config
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# run copyparty # run copyparty
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.19.16/copyparty-1.19.16.tar.gz", "url": "https://github.com/9001/copyparty/releases/download/v1.19.2/copyparty-1.19.2.tar.gz",
"version": "1.19.16", "version": "1.19.2",
"hash": "sha256-2MwQ02I+rMyKyuvdOwECoev4eKA//m5zfRMsZveZ1oI=" "hash": "sha256-nw3NgSTyYKDHJna3DYTII4jP5bR+fQVW9RkMiCCFgKI="
} }

View file

@ -1,11 +0,0 @@
final: prev: {
copyparty = final.python3.pkgs.callPackage ./copyparty {
ffmpeg = final.ffmpeg-full;
};
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./partftpy { };
};
};
}

View file

@ -101,10 +101,10 @@
function our_hotkey_handler(e) { function our_hotkey_handler(e) {
// bail if either ALT, CTRL, or SHIFT is pressed // bail if either ALT, CTRL, or SHIFT is pressed
if (anymod(e)) if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
return main_hotkey_handler(e); // let copyparty handle this keystroke return main_hotkey_handler(e); // let copyparty handle this keystroke
var keycode = (e.key || e.code) + '', var key_name = (e.code || e.key) + '',
ae = document.activeElement, ae = document.activeElement,
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
@ -114,7 +114,7 @@
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet)) if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
return main_hotkey_handler(e); // let copyparty handle this keystroke return main_hotkey_handler(e); // let copyparty handle this keystroke
if (keycode == 'w' || keycode == 'KeyW') { if (key_name == 'KeyW') {
// okay, this one's for us... do the thing // okay, this one's for us... do the thing
action_to_perform(); action_to_perform();
return ev(e); return ev(e);

View file

@ -1,173 +0,0 @@
# copyparty with Podman and Systemd
Use this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service.
Documentation for `.container` files can be found in the [Container unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container) docs. Systemd does not understand `.container` files natively, so Podman converts these to `.service` files with a [systemd-generator](https://www.freedesktop.org/software/systemd/man/latest/systemd.generator.html). This process is transparent, but sometimes needs to be debugged in case your `.container` file is malformed. There are instructions to debug the systemd generator in the Troubleshooting section below.
To run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation
There is a sample configuration file in the same directory as this file (`copyparty.conf`).
## Run the container as root
Running the container as the root user is easy to set up, but less secure. There are instructions in the next section to run the container as a rootless user if you'd rather run the container like that.
First, change this line in the `copyparty.container` file to reflect the directory you want to share. By default, it shares `/mnt/` but you'll probably want to change that.
```
# Change /mnt to something you want to share
Volume=/mnt:/w:z
```
Note that you can select the owner and group of this volume by changing the `uid:` and `gid:` of the volume in `copyparty.conf`, but for simplicity let's assume you want it to be owned by `root:root`.
To install and start copyparty with Podman and systemd as the root user, run the following:
```shell
sudo mkdir -pv /etc/systemd/container/ /etc/copyparty/
sudo cp -v copyparty.container /etc/systemd/containers/
sudo cp -v copyparty.conf /etc/copyparty/
sudo systemctl daemon-reload
sudo systemctl start copyparty
```
Note: You can't "enable" this kind of Podman service. The `[Install]` section of the `.container` file effectively handles enabling the service so that it starts when the server reboots.
You can see the status of the service with:
```shell
sudo systemctl status -a copyparty
```
You can see (and follow) the logs with either of these commands:
```shell
sudo podman logs -f copyparty
# -a is required or else you'll get output like: copyparty[549025]: [649B blob data]
sudo journalctl -a -f -u copyparty
```
## Run the container as a non-root user
This configuration is more secure, but is more involved and requires ensuring files have proper permissions. You will need a root user account to do some of this setup.
First, you need a user to run the container as. In this example we'll create a "podman" user with UID=1001 and GID=1001.
```shell
sudo groupadd -g 1001 podman
sudo useradd -u 1001 -m podman
sudo usermod -aG podman podman
sudo loginctl enable-linger podman
# Set a strong password for this user
sudo -u podman passwd
```
The `enable-linger` command allows the podman user to run systemd user services that persist even when the user is not logged in. You could use a user that already exists in the system to run this service as, just make sure to run `loginctl enable-linger USERNAME` for that user.
Next, change these lines in the `copyparty.container` file to reflect the config directory and the directory you want to share. By default, the config shares `/home/podman/copyparty/sharing/` but you'll probably want to change this:
```
# Change to reflect your non-root user's home directory
Volume=/home/podman/copyparty/config:/cfg:z
# Change to the directory you want to share
Volume=/home/podman/copyparty/sharing:/w:z
```
Make sure the podman user has read/write access to both of these directories.
Next, **log in to the server as the podman user**.
To install and start copyparty as the non-root podman user, run the following:
```shell
mkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config
cp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container
cp -v copyparty.conf /home/podman/copyparty/config
systemctl --user daemon-reload
systemctl --user start copyparty
```
**Important note: Never use `sudo` with `systemctl --user`!**
You can check the status of the user service with:
```shell
systemctl --user status -a copyparty
```
You can see (and follow) the logs with:
```shell
podman logs -f copyparty
journalctl --user -a -f -u copyparty
```
## Troubleshooting
If the container fails to start, and you've modified the `.container` service, it's likely that your `.container` file failed to be translated into a `.service` file. You can debug the podman service generator with this command:
```shell
sudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun
```
## Allowing Traffic from Outside your Server
To allow traffic on port 3923 of your server, you should run:
```shell
sudo firewall-cmd --permanent --add-port=3923/tcp
sudo firewall-cmd --reload
```
Otherwise, you won't be able to access the copyparty server from anywhere other than the server itself.
## Updating copyparty
To update the version of copyparty used in the container, you can:
```shell
# If root:
sudo podman pull docker.io/copyparty/ac:latest
sudo systemctl restart copyparty
# If non-root:
podman pull docker.io/copyparty/ac:latest
systemctl --user restart copyparty
```
Or, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run:
```shell
# If root:
sudo systemctl daemon-reload
sudo systemctl restart copyparty
# If non-root:
systemctl --user daemon-reload
systemctl --user restart copyparty
```
Podman will pull the image you've specified when restarting. If you have it set to `:latest`, Podman does not know to re-pull the container.
### Enabling auto-update
Alternatively, you can enable auto-updates by un-commenting this line:
```
# AutoUpdate=registry
```
You will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with:
```shell
# If root:
sudo systemctl enable podman-auto-update.timer podman-auto-update.service
# If non-root:
systemctl --user enable podman-auto-update.timer podman-auto-update.service
```
This works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours.

View file

@ -1,36 +0,0 @@
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
ansi # and colors in log messages
# uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl;
# $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
/w # share the contents of the "/w" folder
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin
# uid: 1000 # If you're running as root, you can change the owner of this volume here
# gid: 1000 # If you're running as root, you can change the group of this volume here

View file

@ -1,55 +0,0 @@
[Container]
# It's recommended to replace :latest with a specific version
# for example: docker.io/copyparty/ac:1.19.15
Image=docker.io/copyparty/ac:latest
ContainerName=copyparty
# Uncomment to enable auto-updates
# AutoUpdate=registry
# Environment variables
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
Environment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
Environment=PYTHONUNBUFFERED=1
# Ports
PublishPort=3923:3923
# Volumes (PLEASE LOOK!)
# Rootful setup:
# Leave as-is
# Non-root setup:
# Change /etc/copyparty to /home/<USER>/copyparty/config
Volume=/etc/copyparty:/cfg:z
# Rootful setup:
# Change /mnt to the directory you want to share
# Non-root setup:
# Change /mnt to something owned by your user, e.g., /home/<USER>/copyparty/sharing:/w:z
Volume=/mnt:/w:z
# Give the container time to stop in case the thumbnailer is still running.
# It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer
StopTimeout=15
# hide it from logs with "/._" so it matches the default --lf-url filter
HealthCmd="wget --spider -q 127.0.0.1:3923/?reset=/._"
HealthInterval=1m
HealthTimeout=2s
HealthRetries=5
HealthStartPeriod=15s
[Unit]
After=default.target
[Install]
# Start by default on boot
WantedBy=default.target
[Service]
# Give the container time to start in case it needs to pull the image
TimeoutStartSec=600

View file

@ -1,71 +0,0 @@
#!/bin/bash
#
# this script will install copyparty onto an iOS device (iPhone/iPad)
#
# step 1: install a-Shell:
# https://apps.apple.com/us/app/a-shell/id1473805438
#
# step 2: copypaste the following command into a-Shell:
# curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh
#
# step 3: launch copyparty with this command: cpp
#
# if you ever want to upgrade copyparty, just repeat step 2
cd "$HOME/Documents"
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
# create the config file? (cannot use heredoc because body too large)
[ -e cpc ] || {
echo '[global]' >cpc
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
echo ' e2ts # and enable multimedia indexing' >>cpc
echo ' ver # show copyparty version in the controlpanel' >>cpc
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
echo ' qrp: 1 # reduce qr-code padding' >>cpc
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
echo '' >>cpc
echo ' # enable these by uncommenting them:' >>cpc
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
echo '' >>cpc
echo '[/]' >>cpc
echo ' ~/Documents' >>cpc
echo ' accs:' >>cpc
echo ' A: *' >>cpc
}
# create the launcher?
[ -e cpp ] || {
echo '#!/bin/sh' >cpp
echo '' >>cpp
echo '# change the font so the qr-code draws correctly:' >>cpp
echo 'config -n "Menlo" # name' >>cpp
echo 'config -s 8 # size' >>cpp
echo '' >>cpp
echo '# launch copyparty' >>cpp
echo 'exec copyparty -c cpc "$@"' >>cpp
}
chmod 755 copyparty cpp
echo
echo =================================
echo
echo 'okay, all done!'
echo
echo 'you can edit your config'
echo 'with this command: vim cpc'
echo
echo 'you can run copyparty'
echo 'with this command: cpp'
echo

View file

@ -88,7 +88,6 @@ web/mde.html
web/mde.js web/mde.js
web/msg.css web/msg.css
web/msg.html web/msg.html
web/opds.xml
web/rups.css web/rups.css
web/rups.html web/rups.html
web/rups.js web/rups.js
@ -112,9 +111,7 @@ class EnvParams(object):
def __init__(self) -> None: def __init__(self) -> None:
self.t0 = time.time() self.t0 = time.time()
self.mod = "" self.mod = ""
self.mod_ = ""
self.cfg = "" self.cfg = ""
self.scfg = True
E = EnvParams() E = EnvParams()

View file

@ -36,7 +36,6 @@ from .__init__ import (
) )
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .bos import bos
from .cfg import flagcats, onedash from .cfg import flagcats, onedash
from .svchub import SvcHub from .svchub import SvcHub
from .util import ( from .util import (
@ -187,7 +186,7 @@ def init_E(EE: EnvParams) -> None:
E = EE # pylint: disable=redefined-outer-name E = EE # pylint: disable=redefined-outer-name
def get_unixdir() -> tuple[str, bool]: def get_unixdir() -> str:
paths: list[tuple[Callable[..., Any], str]] = [ paths: list[tuple[Callable[..., Any], str]] = [
(os.environ.get, "XDG_CONFIG_HOME"), (os.environ.get, "XDG_CONFIG_HOME"),
(os.path.expanduser, "~/.config"), (os.path.expanduser, "~/.config"),
@ -197,57 +196,41 @@ def init_E(EE: EnvParams) -> None:
(unicode, "/tmp"), (unicode, "/tmp"),
] ]
errs = [] errs = []
for chk in [os.listdir, os.mkdir]:
for npath, (pf, pa) in enumerate(paths): for npath, (pf, pa) in enumerate(paths):
priv = npath < 2 # private/trusted location
ram = npath > 1 # "nonvolatile"; not semantically same as `not priv`
p = "" p = ""
try: try:
p = pf(pa) p = pf(pa)
# print(chk.__name__, p, pa)
if not p or p.startswith("~"): if not p or p.startswith("~"):
continue continue
p = os.path.normpath(p) p = os.path.normpath(p)
mkdir = not os.path.isdir(p) chk(p) # type: ignore
if mkdir:
os.mkdir(p, 0o700)
p = os.path.join(p, "copyparty") p = os.path.join(p, "copyparty")
if not priv and os.path.isdir(p): if not os.path.isdir(p):
uid = os.geteuid() os.mkdir(p)
if os.stat(p).st_uid != uid:
p += ".%s" % (uid,)
if os.path.isdir(p) and os.stat(p).st_uid != uid:
raise Exception("filesystem has broken unix permissions")
try:
os.listdir(p)
except:
os.mkdir(p, 0o700)
if ram: if npath > 1:
t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
errs.append(t % (pa, p)) errs.append(t % (p,))
elif mkdir:
t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)"
errs.append(t % (pa, p, " instead" if npath else "", pa))
elif errs: elif errs:
errs.append("Using %s/copyparty [%s] instead" % (pa, p)) errs.append("Using [%s] instead" % (p,))
if errs: if errs:
warn(". ".join(errs)) warn(". ".join(errs))
return p, priv return p # type: ignore
except Exception as ex: except Exception as ex:
if p: if p and npath < 2:
t = "Unable to store config in %s [%s] due to %r" t = "Unable to store config in [%s] due to %r"
errs.append(t % (pa, p, ex)) errs.append(t % (p, ex))
t = "could not find a writable path for runtime state:\n> %s" raise Exception("could not find a writable path for config")
raise Exception(t % ("\n> ".join(errs)))
E.mod = os.path.dirname(os.path.realpath(__file__)) E.mod = os.path.dirname(os.path.realpath(__file__))
if E.mod.endswith("__init__"): if E.mod.endswith("__init__"):
E.mod = os.path.dirname(E.mod) E.mod = os.path.dirname(E.mod)
E.mod_ = os.path.join(E.mod, "")
try: try:
p = os.environ.get("XDG_CONFIG_HOME") p = os.environ.get("XDG_CONFIG_HOME")
@ -258,7 +241,7 @@ def init_E(EE: EnvParams) -> None:
p = os.path.abspath(os.path.realpath(p)) p = os.path.abspath(os.path.realpath(p))
p = os.path.join(p, "copyparty") p = os.path.join(p, "copyparty")
if not os.path.isdir(p): if not os.path.isdir(p):
os.mkdir(p, 0o700) os.mkdir(p)
os.listdir(p) os.listdir(p)
except: except:
p = "" p = ""
@ -271,11 +254,11 @@ def init_E(EE: EnvParams) -> None:
elif sys.platform == "darwin": elif sys.platform == "darwin":
E.cfg = os.path.expanduser("~/Library/Preferences/copyparty") E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
else: else:
E.cfg, E.scfg = get_unixdir() E.cfg = get_unixdir()
E.cfg = E.cfg.replace("\\", "/") E.cfg = E.cfg.replace("\\", "/")
try: try:
bos.makedirs(E.cfg, bos.MKD_700) os.makedirs(E.cfg)
except: except:
if not os.path.isdir(E.cfg): if not os.path.isdir(E.cfg):
raise raise
@ -456,7 +439,7 @@ def args_from_cfg(cfg_path: str) -> list[str]:
def expand_cfg(argv) -> list[str]: def expand_cfg(argv) -> list[str]:
if CFG_DEF: if CFG_DEF:
supp = args_from_cfg(CFG_DEF[0]) supp = args_from_cfg(CFG_DEF[0])
argv = argv[:1] + supp + argv[1:] argv = supp + argv
n = 0 n = 0
while n < len(argv): while n < len(argv):
@ -687,42 +670,6 @@ def get_sects():
""" """
), ),
], ],
[
"auth-ord",
"authentication precedence",
dedent(
"""
\033[33m--auth-ord\033[0m is a comma-separated list of auth options
(one or more of the [\033[35moptions\033[0m] below); first one wins
[\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header,
or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie
(see \033[33m--help-auth\033[0m)
[\033[35midp\033[0m] is a username provided in the http-request-header
defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is
provided by an authentication middleware such as
authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m)
[\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header,
[\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header;
[\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m]
[\033[35mipu\033[0m] is a mapping from an IP-address to a username,
auto-authing that client-IP to that account
(see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m)
NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list,
it may still be enabled and can still take effect if
none of the other alternatives identify the user
NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST
NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button
will be hidden when any idp feature is enabled
"""
),
],
[ [
"flags", "flags",
"list of volflags", "list of volflags",
@ -813,11 +760,9 @@ def get_sects():
\033[36mf\033[35m forks the process, doesn't wait for completion \033[36mf\033[35m forks the process, doesn't wait for completion
\033[36mc\033[35m checks return code, blocks the action if non-zero \033[36mc\033[35m checks return code, blocks the action if non-zero
\033[36mj\033[35m provides json with info as 1st arg instead of filepath \033[36mj\033[35m provides json with info as 1st arg instead of filepath
\033[36ms\033[35m provides input data on stdin (instead of 1st arg)
\033[36mwN\033[35m waits N sec after command has been started before continuing \033[36mwN\033[35m waits N sec after command has been started before continuing
\033[36mtN\033[35m sets an N sec timeout before the command is abandoned \033[36mtN\033[35m sets an N sec timeout before the command is abandoned
\033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5) \033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5)
\033[36mI\033[35m import and run as module, not as subprocess
\033[36mar\033[35m only run hook if user has read-access \033[36mar\033[35m only run hook if user has read-access
\033[36marw\033[35m only run hook if user has read-write-access \033[36marw\033[35m only run hook if user has read-write-access
@ -830,7 +775,7 @@ def get_sects():
\033[36mc0\033[35m show all process output (default) \033[36mc0\033[35m show all process output (default)
\033[36mc1\033[35m show only stderr \033[36mc1\033[35m show only stderr
\033[36mc2\033[35m show only stdout \033[36mc2\033[35m show only stdout
\033[36mc3\033[35m mute all process output \033[36mc3\033[35m mute all process otput
\033[0m \033[0m
examples: examples:
@ -847,9 +792,6 @@ def get_sects():
the \033[33m--\033[35m stops notify-send from reading the message as args the \033[33m--\033[35m stops notify-send from reading the message as args
and the alert will be "hey" followed by the messagetext and the alert will be "hey" followed by the messagetext
\033[36m--xm s,,tee,-a,log.txt\033[35m appends each msg to log.txt;
\033[36m--xm s,j,,tee,-a,log.txt\033[35m writes it as json instead
\033[36m--xau zmq:pub:tcp://*:5556\033[35m announces uploads on zeromq; \033[36m--xau zmq:pub:tcp://*:5556\033[35m announces uploads on zeromq;
\033[36m--xau t3,zmq:push:tcp://*:5557\033[35m also works, and you can \033[36m--xau t3,zmq:push:tcp://*:5557\033[35m also works, and you can
\033[36m--xau t3,j,zmq:req:tcp://localhost:5555\033[35m too for example \033[36m--xau t3,j,zmq:req:tcp://localhost:5555\033[35m too for example
@ -859,8 +801,7 @@ def get_sects():
as soon as the volume has been idle for iN seconds (5 by default) as soon as the volume has been idle for iN seconds (5 by default)
\033[36mxiu\033[0m is also unique in that it will pass the metadata to the \033[36mxiu\033[0m is also unique in that it will pass the metadata to the
executed program on STDIN instead of as argv arguments (so executed program on STDIN instead of as argv arguments, and
just like the \033[36ms\033[0m option does for the other hook types), and
it also includes the wark (file-id/hash) as a json property it also includes the wark (file-id/hash) as a json property
\033[36mxban\033[0m can be used to overrule / cancel a user ban event; \033[36mxban\033[0m can be used to overrule / cancel a user ban event;
@ -871,12 +812,6 @@ def get_sects():
on new uploads, but with certain limitations. See on new uploads, but with certain limitations. See
bin/hooks/reloc* and docs/devnotes.md#hook-effects bin/hooks/reloc* and docs/devnotes.md#hook-effects
the \033[36mI\033[0m option will override most other options, because
it entirely hands over control to the hook, which is
then able to tamper with copyparty's internal memory
and wreck havoc if it wants to -- but this is worh it
because it makes the hook 140x faster
except for \033[36mxm\033[0m, only one hook / one action can run at a time, except for \033[36mxm\033[0m, only one hook / one action can run at a time,
so it's recommended to use the \033[36mf\033[0m flag unless you really need so it's recommended to use the \033[36mf\033[0m flag unless you really need
to wait for the hook to finish before continuing (without \033[36mf\033[0m to wait for the hook to finish before continuing (without \033[36mf\033[0m
@ -910,11 +845,6 @@ def get_sects():
middleware and not by clients! and, as an extra precaution, middleware and not by clients! and, as an extra precaution,
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword) send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
the login/logout links/buttons can be replaced with links
going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m
will expand \033[36m{dst}\033[0m to the URL of the current page, so
the IdP can redirect the user back to where they were
""" """
), ),
], ],
@ -968,7 +898,7 @@ def get_sects():
\033[36m{{vf.thsize}} \033[35mthumbnail size \033[36m{{vf.thsize}} \033[35mthumbnail size
\033[36m{{srv.itime}} \033[35mserver time in seconds \033[36m{{srv.itime}} \033[35mserver time in seconds
\033[36m{{srv.htime}} \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC) \033[36m{{srv.htime}} \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC)
\033[36m{{hdr.cf-ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank) \033[36m{{hdr.cf_ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
\033[0m \033[0m
so the following types of placeholders can be added to the lists: so the following types of placeholders can be added to the lists:
* any client header can be accessed through {{hdr.*}} * any client header can be accessed through {{hdr.*}}
@ -1162,13 +1092,10 @@ def add_general(ap, nc, srvname):
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m 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], see --help-accounts") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m 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], see --help-accounts")
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]") ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password") ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes")
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("-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,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", 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("--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("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)")
ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS)
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
@ -1187,18 +1114,11 @@ def add_qr(ap, tty):
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)") ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
def add_fs(ap): def add_fs(ap):
ap2 = ap.add_argument_group("filesystem options") ap2 = ap.add_argument_group("filesystem options")
rm_re_def = "15/0.1" if ANYWIN else "0/0" rm_re_def = "15/0.1" if ANYWIN else "0/0"
ap2.add_argument("--casechk", metavar="N", type=u, default="auto", help="detect and prevent CI (case-insensitive) behavior if the underlying filesystem is CI? [\033[32my\033[0m] = detect and prevent, [\033[32mn\033[0m] = ignore and allow, [\033[32mauto\033[0m] = \033[32my\033[0m if CI fs detected. NOTE: \033[32my\033[0m is very slow but necessary for correct WebDAV behavior on Windows/Macos (volflag=casechk)")
ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)") ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)") ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)") ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
@ -1210,7 +1130,6 @@ def add_share(ap):
ap2 = ap.add_argument_group("share-url options") ap2 = ap.add_argument_group("share-url options")
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]") ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in") ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)")
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share") ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week") ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
ap2.add_argument("--shr-v", action="store_true", help="debug") ap2.add_argument("--shr-v", action="store_true", help="debug")
@ -1234,7 +1153,6 @@ def add_upload(ap):
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)") ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)") ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)") ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)") ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)") ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)") ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
@ -1245,9 +1163,7 @@ def add_upload(ap):
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("--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 \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash") 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.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)") ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
ap2.add_argument("--rm-partial", action="store_true", help="delete the .PARTIAL file when an unfinished upload expires after \033[33m--snap-drop\033[0m (volflag=rm_partial)")
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("--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("--rotf-tz", metavar="TXT", type=u, default="UTC", help="default timezone for the rotf upload rule; examples: [\033[32mEurope/Oslo\033[0m], [\033[32mAmerica/Toronto\033[0m], [\033[32mAntarctica/South_Pole\033[0m] (volflag=rotf_tz)")
ap2.add_argument("--rand", action="store_true", help="force randomized filenames, \033[33m--nrand\033[0m 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("--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("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
@ -1272,7 +1188,6 @@ def add_network(ap):
ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. 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("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. 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="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\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]") 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]")
ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets")
if ANYWIN: 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") 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")
elif not MACOS: elif not MACOS:
@ -1323,7 +1238,7 @@ def add_auth(ap):
ses_db = os.path.join(E.cfg, "sessions.db") ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options") ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (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") ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (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")
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp") ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
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") 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")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
@ -1331,10 +1246,6 @@ def add_auth(ap):
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)") ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
@ -1345,10 +1256,6 @@ def add_auth(ap):
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]") ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)
def add_chpw(ap): def add_chpw(ap):
@ -1380,8 +1287,6 @@ def add_zc_mdns(ap):
ap2.add_argument("--zm6", action="store_true", help="IPv6 only") ap2.add_argument("--zm6", action="store_true", help="IPv6 only")
ap2.add_argument("--zmv", action="store_true", help="verbose mdns") ap2.add_argument("--zmv", action="store_true", help="verbose mdns")
ap2.add_argument("--zmvv", action="store_true", help="verboser mdns") ap2.add_argument("--zmvv", action="store_true", help="verboser mdns")
ap2.add_argument("--zm-http", metavar="PORT", type=int, default=-1, help="port to announce for http/webdav; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649")
ap2.add_argument("--zm-https", metavar="PORT", type=int, default=-1, help="port to announce for https/webdavs; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649")
ap2.add_argument("--zm-no-pe", action="store_true", help="mute parser errors (invalid incoming MDNS packets)") ap2.add_argument("--zm-no-pe", action="store_true", help="mute parser errors (invalid incoming MDNS packets)")
ap2.add_argument("--zm-nwa-1", action="store_true", help="disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)") ap2.add_argument("--zm-nwa-1", action="store_true", help="disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)")
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("--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)")
@ -1455,10 +1360,6 @@ def add_smb(ap):
ap2.add_argument("--smbvv", action="store_true", help="verboser") ap2.add_argument("--smbvv", action="store_true", help="verboser")
ap2.add_argument("--smbvvv", action="store_true", help="verbosest") ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
def add_opds(ap):
ap2 = ap.add_argument_group("OPDS options")
ap2.add_argument("--opds", action="store_true", help="enable opds -- allows e-book readers to browse and download files (volflag=opds)")
ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")
def add_handlers(ap): def add_handlers(ap):
ap2 = ap.add_argument_group("handlers (see --help-handlers)") ap2 = ap.add_argument_group("handlers (see --help-handlers)")
@ -1500,7 +1401,6 @@ def add_yolo(ap):
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI") ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
def add_optouts(ap): def add_optouts(ap):
@ -1514,7 +1414,7 @@ def add_optouts(ap):
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move") ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m 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("-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. This is the same as --du-who no") 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("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)") ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)") ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
@ -1533,7 +1433,7 @@ def add_optouts(ap):
def add_safety(ap): def add_safety(ap):
ap2 = ap.add_argument_group("safety options") ap2 = ap.add_argument_group("safety options")
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("-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 requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --reflink --dav-auth --vague-403 -nih") ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --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("-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, default="", 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("--ls", metavar="U[,V[,F]]", type=u, default="", 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("--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)")
@ -1553,7 +1453,7 @@ def add_safety(ap):
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-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("--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("--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\..{3}|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 403/404-bans; 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("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy") ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies") ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header") ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
@ -1595,15 +1495,11 @@ def add_logging(ap):
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)") ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") 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=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") 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("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
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("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m") ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
ap2.add_argument("--scan-st-r", metavar="SEC", type=float, default=0.1, help="fs-indexing: wait \033[33mSEC\033[0m between each status-message")
ap2.add_argument("--scan-pr-r", metavar="SEC", type=float, default=10, help="fs-indexing: wait \033[33mSEC\033[0m between each 'progress:' message")
ap2.add_argument("--scan-pr-s", metavar="MiB", type=float, default=1, help="fs-indexing: say 'file: <name>' when a file larger than \033[33mMiB\033[0m is about to be hashed")
def add_admin(ap): def add_admin(ap):
@ -1711,7 +1607,6 @@ def add_db_general(ap, hcores):
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("--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="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)") 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.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)") ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
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-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("--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("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)") ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
@ -1737,7 +1632,6 @@ def add_db_metadata(ap):
def add_txt(ap): def add_txt(ap):
ap2 = ap.add_argument_group("textfile options") ap2 = ap.add_argument_group("textfile options")
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)") ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)") ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
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("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
@ -1766,13 +1660,12 @@ def add_og(ap):
ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m") ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m")
def add_ui(ap, retry: int): def add_ui(ap, retry):
THEMES = 10 THEMES = 10
ap2 = ap.add_argument_group("ui options") 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("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,)) ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed") ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
@ -1784,7 +1677,6 @@ def add_ui(ap, retry: int):
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)") ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: 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/folders matching \033[33mREGEX\033[0m in file list. WARNING: 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("--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("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)")
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)") ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]") ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]")
@ -1792,18 +1684,13 @@ def add_ui(ap, retry: int):
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html") ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages") ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)") ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)") ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext") ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
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("--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("--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("--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=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all") ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once") ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
@ -1844,7 +1731,7 @@ def add_debug(ap):
def run_argparse( def run_argparse(
argv: list[str], formatter: Any, retry: int, nc: int, verbose=True argv: list[str], formatter: Any, retry: bool, nc: int, verbose=True
) -> argparse.Namespace: ) -> argparse.Namespace:
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
formatter_class=formatter, formatter_class=formatter,
@ -1890,7 +1777,6 @@ def run_argparse(
add_webdav(ap) add_webdav(ap)
add_tftp(ap) add_tftp(ap)
add_smb(ap) add_smb(ap)
add_opds(ap)
add_safety(ap) add_safety(ap)
add_salt(ap, fk_salt, dk_salt, ah_salt) add_salt(ap, fk_salt, dk_salt, ah_salt)
add_optouts(ap) add_optouts(ap)
@ -1912,19 +1798,16 @@ def run_argparse(
for k, h, _ in sects: for k, h, _ in sects:
ap2.add_argument("--help-" + k, action="store_true", help=h) ap2.add_argument("--help-" + k, action="store_true", help=h)
if retry:
a = ["ascii", "replace"]
for x in ap._actions:
try: try:
x.default = x.default.encode(*a).decode(*a) if not retry:
except: raise Exception()
pass
try: for x in ap._actions:
if x.help and x.help is not argparse.SUPPRESS: if not x.help:
x.help = x.help.replace("└─", "`-").encode(*a).decode(*a) continue
if retry > 2:
x.help = RE_ANSI.sub("", x.help) a = ["ascii", "replace"]
x.help = x.help.encode(*a).decode(*a) + "\033[0m"
except: except:
pass pass
@ -2036,7 +1919,7 @@ def main(argv: Optional[list[str]] = None) -> None:
except: except:
nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows
retry = 0 retry = False
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]: for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
try: try:
al = run_argparse(argv, fmtr, retry, nc) al = run_argparse(argv, fmtr, retry, nc)
@ -2045,9 +1928,8 @@ def main(argv: Optional[list[str]] = None) -> None:
except SystemExit: except SystemExit:
raise raise
except: except:
retry += 1 retry = True
t = "WARNING: due to limitations in your terminal and/or OS, the helptext cannot be displayed correctly. Will show a simplified version due to the following error:\n[ %s ]:\n%s\n" lprint("\n[ {} ]:\n{}\n".format(fmtr, min_ex()))
lprint(t % (fmtr, min_ex()))
try: try:
assert al # type: ignore assert al # type: ignore
@ -2056,9 +1938,6 @@ def main(argv: Optional[list[str]] = None) -> None:
except: except:
sys.exit(1) sys.exit(1)
if al.chdir:
os.chdir(al.chdir)
if al.ansi: if al.ansi:
al.no_ansi = False al.no_ansi = False
elif not al.no_ansi: elif not al.no_ansi:
@ -2086,7 +1965,7 @@ def main(argv: Optional[list[str]] = None) -> None:
if not HAVE_IPV6 and al.i == "::": if not HAVE_IPV6 and al.i == "::":
al.i = "0.0.0.0" al.i = "0.0.0.0"
al.i = [x.strip() for x in al.i.split(",")] al.i = al.i.split(",")
try: try:
if "-" in al.p: if "-" in al.p:
lo, hi = [int(x) for x in al.p.split("-")] lo, hi = [int(x) for x in al.p.split("-")]

View file

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 19, 16) VERSION = (1, 19, 3)
CODENAME = "usernames" CODENAME = "usernames"
BUILD_DT = (2025, 10, 5) BUILD_DT = (2025, 8, 17)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View file

@ -13,7 +13,7 @@ import threading
import time import time
from datetime import datetime from datetime import datetime
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
from .bos import bos from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash from .pwhash import PWHash
@ -21,10 +21,8 @@ from .util import (
DEF_MTE, DEF_MTE,
DEF_MTH, DEF_MTH,
EXTS, EXTS,
FAVICON_MIMES,
HAVE_SQLITE3, HAVE_SQLITE3,
IMPLICATIONS, IMPLICATIONS,
META_NOBOTS,
MIMES, MIMES,
SQLITE_VER, SQLITE_VER,
UNPLICATIONS, UNPLICATIONS,
@ -101,8 +99,6 @@ SBADCFG = " ({})".format(BAD_CFG)
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}") PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}") PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
PTN_U_ANY = re.compile(r"(\${[u][}%])")
PTN_G_ANY = re.compile(r"(\${[g][}%])")
PTN_SIGIL = re.compile(r"(\${[ug][}%])") PTN_SIGIL = re.compile(r"(\${[ug][}%])")
@ -171,19 +167,14 @@ class Lim(object):
self.rotn = 0 # rot num files self.rotn = 0 # rot num files
self.rotl = 0 # rot depth self.rotl = 0 # rot depth
self.rotf = "" # rot datefmt self.rotf = "" # rot datefmt
self.rotf_tz = UTC # rot timezone
self.rot_re = re.compile("") # rotf check self.rot_re = re.compile("") # rotf check
def log(self, msg: str, c: Union[int, str] = 0) -> None: def log(self, msg: str, c: Union[int, str] = 0) -> None:
if self.log_func: if self.log_func:
self.log_func("up-lim", msg, c) self.log_func("up-lim", msg, c)
def set_rotf(self, fmt: str, tz: str) -> None: def set_rotf(self, fmt: str) -> None:
self.rotf = fmt self.rotf = fmt
if tz != "UTC":
from zoneinfo import ZoneInfo
self.rotf_tz = ZoneInfo(tz)
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}") r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r) r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
self.rot_re = re.compile("(^|/)" + r + "$") self.rot_re = re.compile("(^|/)" + r + "$")
@ -287,7 +278,7 @@ class Lim(object):
if self.rot_re.search(path.replace("\\", "/")): if self.rot_re.search(path.replace("\\", "/")):
return path, "" return path, ""
suf = datetime.now(self.rotf_tz).strftime(self.rotf) suf = datetime.now(UTC).strftime(self.rotf)
if path: if path:
path += "/" path += "/"
@ -433,14 +424,10 @@ class VFS(object):
self.all_nodes[vpath] = self self.all_nodes[vpath] = self
self.all_aps = [(rp, [self])] self.all_aps = [(rp, [self])]
self.all_vps = [(vp, self)] self.all_vps = [(vp, self)]
self.canonical = self._canonical
self.dcanonical = self._dcanonical
else: else:
self.histpath = self.dbpath = "" self.histpath = self.dbpath = ""
self.all_aps = [] self.all_aps = []
self.all_vps = [] self.all_vps = []
self.canonical = self._canonical_null
self.dcanonical = self._dcanonical_null
self.get_dbv = self._get_dbv self.get_dbv = self._get_dbv
self.ls = self._ls self.ls = self._ls
@ -637,39 +624,7 @@ class VFS(object):
vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem) vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
return dbv, vrem return dbv, vrem
def casechk(self, rem: str, do_stat: bool) -> bool: def canonical(self, rem: str, resolve: bool = True) -> str:
ap = self.canonical(rem, False)
if do_stat and not bos.path.exists(ap):
return True # doesn't exist at all; good to go
dp, fn = os.path.split(ap)
if not fn:
return True # filesystem root
try:
fns = os.listdir(dp)
except:
return True # maybe chmod 111; assume ok
if fn in fns:
return True
hit = "<?>"
lfn = fn.lower()
for zs in fns:
if lfn == zs.lower():
hit = zs
break
if not hit:
return True # NFC/NFD or something, can't be helped either way
if self.log:
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
self.log("vfs", t % (fn, hit))
return False
def _canonical_null(self, rem: str, resolve: bool = True) -> str:
return ""
def _dcanonical_null(self, rem: str) -> str:
return ""
def _canonical(self, rem: str, resolve: bool = True) -> str:
"""returns the canonical path (fully-resolved absolute fs path)""" """returns the canonical path (fully-resolved absolute fs path)"""
ap = self.realpath ap = self.realpath
if rem: if rem:
@ -677,7 +632,7 @@ class VFS(object):
return absreal(ap) if resolve else ap return absreal(ap) if resolve else ap
def _dcanonical(self, rem: str) -> str: def dcanonical(self, rem: str) -> str:
"""resolves until the final component (filename)""" """resolves until the final component (filename)"""
ap = self.realpath ap = self.realpath
if rem: if rem:
@ -686,44 +641,6 @@ class VFS(object):
ad, fn = os.path.split(ap) ad, fn = os.path.split(ap)
return os.path.join(absreal(ad), fn) return os.path.join(absreal(ad), fn)
def _canonical_shr(self, rem: str, resolve: bool = True) -> str:
"""returns the canonical path (fully-resolved absolute fs path)"""
ap = self.realpath
if rem:
ap += "/" + rem
rap = absreal(ap)
if self.shr_files:
assert self.shr_src # !rm
vn, rem = self.shr_src
chk = absreal(os.path.join(vn.realpath, rem))
if chk != rap:
# not the dir itself; assert file allowed
ad, fn = os.path.split(rap)
if chk != ad or fn not in self.shr_files:
return "\n\n"
return rap if resolve else ap
def _dcanonical_shr(self, rem: str) -> str:
"""resolves until the final component (filename)"""
ap = self.realpath
if rem:
ap += "/" + rem
ad, fn = os.path.split(ap)
ad = absreal(ad)
if self.shr_files:
assert self.shr_src # !rm
vn, rem = self.shr_src
chk = absreal(os.path.join(vn.realpath, rem))
if chk != absreal(ap):
# not the dir itself; assert file allowed
if ad != chk or fn not in self.shr_files:
return "\n\n"
return os.path.join(ad, fn)
def _ls_nope( def _ls_nope(
self, *a, **ka self, *a, **ka
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
@ -756,12 +673,8 @@ class VFS(object):
"""return user-readable [fsdir,real,virt] items at vpath""" """return user-readable [fsdir,real,virt] items at vpath"""
virt_vis = {} # nodes readable by user virt_vis = {} # nodes readable by user
abspath = self.canonical(rem) abspath = self.canonical(rem)
if abspath:
real = list(statdir(self.log, scandir, lstat, abspath, throw)) real = list(statdir(self.log, scandir, lstat, abspath, throw))
real.sort() real.sort()
else:
real = []
if not rem: if not rem:
# no vfs nodes in the list of real inodes # no vfs nodes in the list of real inodes
real = [x for x in real if x[0] not in self.nodes] real = [x for x in real if x[0] not in self.nodes]
@ -1063,14 +976,6 @@ class AuthSrv(object):
self.indent = "" self.indent = ""
self.is_lxc = args.c == ["/z/initcfg"] self.is_lxc = args.c == ["/z/initcfg"]
self._vf0b = {
"tcolor": self.args.tcolor,
"du_iwho": self.args.du_iwho,
"shr_who": self.args.shr_who if self.args.shr else "no",
}
self._vf0 = self._vf0b.copy()
self._vf0["d2d"] = True
# fwd-decl # fwd-decl
self.vfs = VFS(log_func, "", "", "", AXS(), {}) self.vfs = VFS(log_func, "", "", "", AXS(), {})
self.acct: dict[str, str] = {} # uname->pw self.acct: dict[str, str] = {} # uname->pw
@ -1109,10 +1014,7 @@ class AuthSrv(object):
yield prev, True yield prev, True
def vf0(self): def vf0(self):
return self._vf0.copy() return {"d2d": True, "tcolor": self.args.tcolor}
def vf0b(self):
return self._vf0b.copy()
def idp_checkin( def idp_checkin(
self, broker: Optional["BrokerCli"], uname: str, gname: str self, broker: Optional["BrokerCli"], uname: str, gname: str
@ -1178,16 +1080,6 @@ class AuthSrv(object):
src0 = src # abspath src0 = src # abspath
dst0 = dst # vpath dst0 = dst # vpath
zsl = []
for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
if bool(ptn.search(src)) != bool(ptn.search(dst)):
zsl.append(sigil)
if zsl:
t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
t = "\n".join([t % (x, src, dst) for x in zsl])
self.log(t, 1)
raise Exception(t)
un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns] un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
if not un_gn: if not un_gn:
# ensure volume creation if there's no users # ensure volume creation if there's no users
@ -1280,8 +1172,8 @@ class AuthSrv(object):
self.log(t, c=3) self.log(t, c=3)
raise Exception(BAD_CFG) raise Exception(BAD_CFG)
if not bos.path.exists(src): if not bos.path.isdir(src):
self.log("warning: filesystem-path did not exist: %r" % (src,), 3) self.log("warning: filesystem-path does not exist: {}".format(src), 3)
mount[dst] = (src, dst0) mount[dst] = (src, dst0)
daxs[dst] = AXS() daxs[dst] = AXS()
@ -1436,10 +1328,6 @@ class AuthSrv(object):
zt = split_cfg_ln(ln) zt = split_cfg_ln(ln)
for zs, za in zt.items(): for zs, za in zt.items():
zs = zs.lstrip("-") zs = zs.lstrip("-")
if "=" in zs:
t = "WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?"
zs1, zs2 = zs.split("=", 1)
self.log(t % (zs, zs1, zs2), 3)
if za is True: if za is True:
self._e("└─argument [{}]".format(zs)) self._e("└─argument [{}]".format(zs))
else: else:
@ -1449,10 +1337,6 @@ class AuthSrv(object):
if cat == cata: if cat == cata:
try: try:
u, p = [zs.strip() for zs in ln.split(":", 1)] u, p = [zs.strip() for zs in ln.split(":", 1)]
if "=" in u and not p:
t = "WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?"
zs1, zs2 = u.split("=", 1)
self.log(t % (u, zs1, zs2), 3)
self._l(ln, 5, "account [{}], password [{}]".format(u, p)) self._l(ln, 5, "account [{}], password [{}]".format(u, p))
acct[u] = p acct[u] = p
except: except:
@ -1523,10 +1407,6 @@ class AuthSrv(object):
zd = split_cfg_ln(ln) zd = split_cfg_ln(ln)
fstr = "" fstr = ""
for sk, sv in zd.items(): for sk, sv in zd.items():
if "=" in sk:
t = "WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?"
zs1, zs2 = sk.split("=", 1)
self.log(t % (sk, zs1, zs2), 3)
bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-") bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-")
if bad: if bad:
err = "bad characters [{}] in volflag name [{}]; " err = "bad characters [{}] in volflag name [{}]; "
@ -1767,7 +1647,6 @@ class AuthSrv(object):
# accept both , and : as separators between usernames # accept both , and : as separators between usernames
zs1, zs2 = x.replace("=", ":").split(":", 1) zs1, zs2 = x.replace("=", ":").split(":", 1)
grps[zs1] = zs2.replace(":", ",").split(",") grps[zs1] = zs2.replace(":", ",").split(",")
grps[zs1] = [x.strip() for x in grps[zs1]]
except: except:
t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...' t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
raise Exception(t.format(x)) raise Exception(t.format(x))
@ -1819,8 +1698,8 @@ class AuthSrv(object):
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns))) self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
raise raise
derive_args(self.args) self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
self.setup_auth_ord() self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
self.setup_pwhash(acct) self.setup_pwhash(acct)
defpw = acct.copy() defpw = acct.copy()
@ -1858,15 +1737,12 @@ class AuthSrv(object):
files = os.listdir(E.cfg) files = os.listdir(E.cfg)
except: except:
files = [] files = []
hits = [ hits = [x for x in files if x.lower().endswith(".conf")]
x
for x in files
if x.lower().endswith(".conf") and not x.startswith(".")
]
if hits: if hits:
t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n" t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n"
self.log(t % (E.cfg, ehint, ", ".join(hits)), 3) self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b()) zvf = {"tcolor": self.args.tcolor}
vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
if not axs.uread: if not axs.uread:
self.badcfg1 = True self.badcfg1 = True
elif "" not in mount: elif "" not in mount:
@ -1902,7 +1778,7 @@ class AuthSrv(object):
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs vol.root = vfs
zs = "neversymlink du_iwho" zs = "neversymlink"
k_ign = set(zs.split()) k_ign = set(zs.split())
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
unknown_flags = set() unknown_flags = set()
@ -2054,8 +1930,6 @@ class AuthSrv(object):
promote = [] promote = []
demote = [] demote = []
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
if not vol.realpath:
continue
hid = self.hid_cache.get(vol.realpath) hid = self.hid_cache.get(vol.realpath)
if not hid: if not hid:
zb = hashlib.sha512(afsenc(vol.realpath)).digest() zb = hashlib.sha512(afsenc(vol.realpath)).digest()
@ -2094,8 +1968,6 @@ class AuthSrv(object):
vol.histpath = absreal(vol.histpath) vol.histpath = absreal(vol.histpath)
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
if not vol.realpath:
continue
hid = self.hid_cache[vol.realpath] hid = self.hid_cache[vol.realpath]
vflag = vol.flags.get("dbpath") vflag = vol.flags.get("dbpath")
if vflag == "-": if vflag == "-":
@ -2224,7 +2096,7 @@ class AuthSrv(object):
zs = vol.flags.get("rotf") zs = vol.flags.get("rotf")
if zs: if zs:
use = True use = True
lim.set_rotf(zs, vol.flags.get("rotf_tz") or "UTC") lim.set_rotf(zs)
zs = vol.flags.get("maxn") zs = vol.flags.get("maxn")
if zs: if zs:
@ -2404,11 +2276,6 @@ class AuthSrv(object):
vol.lim.uid = vol.flags["uid"] vol.lim.uid = vol.flags["uid"]
vol.lim.gid = vol.flags["gid"] vol.lim.gid = vol.flags["gid"]
vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
if not enshare:
vol.flags["shr_who"] = self.args.shr_who = "no"
if vol.flags.get("og"): if vol.flags.get("og"):
self.args.uqe = True self.args.uqe = True
@ -2495,45 +2362,6 @@ class AuthSrv(object):
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s" t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
self.log(t % (vol.vpath, etv), 3) self.log(t % (vol.vpath, etv), 3)
zs = str(vol.flags.get("html_head") or "")
if zs and zs[:1] in "%@":
vol.flags["html_head_d"] = zs
head_s = str(vol.flags.get("html_head_s") or "")
else:
zs2 = str(vol.flags.get("html_head_s") or "")
if zs2 and zs:
head_s = "%s\n%s\n" % (zs2.strip(), zs.strip())
else:
head_s = zs2 or zs
if head_s and not head_s.endswith("\n"):
head_s += "\n"
if "norobots" in vol.flags:
head_s += META_NOBOTS
ico_url = vol.flags.get("ufavico")
if ico_url:
ico_h = ""
ico_ext = ico_url.split("?")[0].split(".")[-1].lower()
if ico_ext in FAVICON_MIMES:
zs = '<link rel="icon" type="%s" href="%s">\n'
ico_h = zs % (FAVICON_MIMES[ico_ext], ico_url)
elif ico_ext == "ico":
zs = '<link rel="shortcut icon" href="%s">\n'
ico_h = zs % (ico_url,)
if ico_h:
vol.flags["ufavico_h"] = ico_h
head_s += ico_h
if head_s:
vol.flags["html_head_s"] = head_s
else:
vol.flags.pop("html_head_s", None)
if not vol.flags.get("html_head_d"):
vol.flags.pop("html_head_d", None)
vol.check_landmarks() vol.check_landmarks()
# d2d drops all database features for a volume # d2d drops all database features for a volume
@ -2623,47 +2451,6 @@ class AuthSrv(object):
self.log(t.format(vol.vpath, mtp), 1) self.log(t.format(vol.vpath, mtp), 1)
errors = True errors = True
for vol in vfs.all_nodes.values():
if not vol.realpath or os.path.isfile(vol.realpath):
continue
ccs = vol.flags["casechk"][:1].lower()
if ccs in ("y", "n"):
if ccs == "y":
vol.flags["bcasechk"] = True
continue
try:
bos.makedirs(vol.realpath, vf=vol.flags)
files = os.listdir(vol.realpath)
for fn in files:
fn2 = fn.lower()
if fn == fn2:
fn2 = fn.upper()
if fn == fn2 or fn2 in files:
continue
is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
ccs = "y" if is_ci else "n"
break
if ccs not in ("y", "n"):
ap = os.path.join(vol.realpath, "casechk")
open(ap, "wb").close()
ccs = "y" if os.path.exists(ap[:-1] + "K") else "n"
os.unlink(ap)
except Exception as ex:
if ANYWIN:
zs = "Windows"
ccs = "y"
elif MACOS:
zs = "Macos"
ccs = "y"
else:
zs = "Linux"
ccs = "n"
t = "unable to determine if filesystem at %r is case-insensitive due to %r; assuming casechk=%s due to %s"
self.log(t % (vol.realpath, ex, ccs, zs), 3)
vol.flags["casechk"] = ccs
if ccs == "y":
vol.flags["bcasechk"] = True
tags = self.args.mtp or [] tags = self.args.mtp or []
tags = [x.split("=")[0] for x in tags] tags = [x.split("=")[0] for x in tags]
tags = [y for x in tags for y in x.split(",")] tags = [y for x in tags for y in x.split(",")]
@ -2767,11 +2554,7 @@ class AuthSrv(object):
if "dedup" in zv.flags: if "dedup" in zv.flags:
have_dedup = True have_dedup = True
if ( if "e2d" not in zv.flags and "hardlink" not in zv.flags:
"e2d" not in zv.flags
and "hardlink" not in zv.flags
and "reflink" not in zv.flags
):
unsafe_dedup.append("/" + zv.vpath) unsafe_dedup.append("/" + zv.vpath)
t += "\n" t += "\n"
@ -2935,22 +2718,13 @@ class AuthSrv(object):
shn.shr_files = set(fns) shn.shr_files = set(fns)
shn.ls = shn._ls_shr shn.ls = shn._ls_shr
shn.canonical = shn._canonical_shr
shn.dcanonical = shn._dcanonical_shr
else: else:
shn.ls = shn._ls shn.ls = shn._ls
shn.canonical = shn._canonical
shn.dcanonical = shn._dcanonical
shn.shr_owner = s_un shn.shr_owner = s_un
shn.shr_src = (s_vfs, s_rem) shn.shr_src = (s_vfs, s_rem)
shn.realpath = s_vfs.canonical(s_rem) shn.realpath = s_vfs.canonical(s_rem)
o_vn, _ = shn._get_share_src("")
shn.flags = o_vn.flags.copy()
shn.dbpath = o_vn.dbpath
shn.histpath = o_vn.histpath
# root.all_aps doesn't include any shares, so make a copy where the # root.all_aps doesn't include any shares, so make a copy where the
# share appears in all abspaths it can provide (for example for chk_ap) # share appears in all abspaths it can provide (for example for chk_ap)
ap = shn.realpath ap = shn.realpath
@ -3008,30 +2782,24 @@ class AuthSrv(object):
"dcrop": vf["crop"], "dcrop": vf["crop"],
"dth3x": vf["th3x"], "dth3x": vf["th3x"],
"u2ts": vf["u2ts"], "u2ts": vf["u2ts"],
"shr_who": vf["shr_who"],
"frand": bool(vf.get("rand")), "frand": bool(vf.get("rand")),
"lifetime": vf.get("lifetime") or 0, "lifetime": vf.get("lifetime") or 0,
"unlist": vf.get("unlist") or "", "unlist": vf.get("unlist") or "",
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"), "sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
} }
if "ufavico_h" in vf:
vn.js_ls["ufavico"] = vf["ufavico_h"]
js_htm = { js_htm = {
"SPINNER": self.args.spinner, "SPINNER": self.args.spinner,
"s_name": self.args.bname, "s_name": self.args.bname,
"idp_login": self.args.idp_login,
"have_up2k_idx": "e2d" in vf, "have_up2k_idx": "e2d" in vf,
"have_acode": not self.args.no_acode, "have_acode": not self.args.no_acode,
"have_c2flac": self.args.allow_flac, "have_c2flac": self.args.allow_flac,
"have_c2wav": self.args.allow_wav, "have_c2wav": self.args.allow_wav,
"have_shr": self.args.shr, "have_shr": self.args.shr,
"shr_who": vf["shr_who"],
"have_zip": not self.args.no_zip, "have_zip": not self.args.no_zip,
"have_mv": not self.args.no_mv, "have_mv": not self.args.no_mv,
"have_del": not self.args.no_del, "have_del": not self.args.no_del,
"have_unpost": int(self.args.unpost), "have_unpost": int(self.args.unpost),
"have_emp": int(self.args.emp), "have_emp": self.args.emp,
"md_no_br": int(vf.get("md_no_br") or 0),
"ext_th": vf.get("ext_th_d") or {}, "ext_th": vf.get("ext_th_d") or {},
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"), "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
"sba_md": vf.get("md_sba") or "", "sba_md": vf.get("md_sba") or "",
@ -3051,7 +2819,6 @@ class AuthSrv(object):
"dvol": self.args.au_vol, "dvol": self.args.au_vol,
"idxh": int(self.args.ih), "idxh": int(self.args.ih),
"dutc": not self.args.localtime, "dutc": not self.args.localtime,
"dfszf": self.args.ui_filesz.strip("-"),
"themes": self.args.themes, "themes": self.args.themes,
"turbolvl": self.args.turbo, "turbolvl": self.args.turbo,
"nosubtle": self.args.nosubtle, "nosubtle": self.args.nosubtle,
@ -3083,18 +2850,6 @@ class AuthSrv(object):
zs = str(vol.flags.get("tcolor") or self.args.tcolor) zs = str(vol.flags.get("tcolor") or self.args.tcolor)
vol.flags["tcolor"] = zs.lstrip("#") vol.flags["tcolor"] = zs.lstrip("#")
def setup_auth_ord(self) -> None:
ao = [x.strip() for x in self.args.auth_ord.split(",")]
if "idp" in ao:
zi = ao.index("idp")
ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:]
zsl = "pw idp-h idp-hm ipu".split()
pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl]
self.args.ao_idp_before_pw = min(h, hm) < pw
self.args.ao_h_before_hm = h < hm
self.args.ao_ipu_wins = ipu == 0
self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs
def load_idp_db(self, quiet=False) -> None: def load_idp_db(self, quiet=False) -> None:
# mutex me # mutex me
level = self.args.idp_store level = self.args.idp_store
@ -3687,35 +3442,6 @@ class AuthSrv(object):
self.log("generated config:\n\n" + "\n".join(ret)) self.log("generated config:\n\n" + "\n".join(ret))
def derive_args(args: argparse.Namespace) -> None:
args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)
args.have_ipu_or_ipr = bool(args.ipu or args.ipr)
def n_du_who(s: str) -> int:
if s == "all":
return 9
if s == "auth":
return 7
if s == "w":
return 5
if s == "rw":
return 4
if s == "a":
return 3
return 0
def n_ver_who(s: str) -> int:
if s == "all":
return 9
if s == "auth":
return 6
if s == "a":
return 3
return 0
def split_cfg_ln(ln: str) -> dict[str, Any]: def split_cfg_ln(ln: str) -> dict[str, Any]:
# "a, b, c: 3" => {a:true, b:true, c:3} # "a, b, c: 3" => {a:true, b:true, c:3}
ret = {} ret = {}
@ -3748,9 +3474,7 @@ def expand_config_file(
if os.path.isdir(fp): if os.path.isdir(fp):
names = list(sorted(os.listdir(fp))) names = list(sorted(os.listdir(fp)))
cnames = [ cnames = [x for x in names if x.lower().endswith(".conf")]
x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
]
if not cnames: if not cnames:
t = "warning: tried to read config-files from folder '%s' but it does not contain any " t = "warning: tried to read config-files from folder '%s' but it does not contain any "
if names: if names:

View file

@ -2,22 +2,18 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import os
import time
from ..util import SYMTIME, fsdec, fsenc from ..util import SYMTIME, fsdec, fsenc
from . import path as path from . import path as path
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union from typing import Any, Optional
from ..util import NamedLogger
MKD_755 = {"chmod_d": 0o755} MKD_755 = {"chmod_d": 0o755}
MKD_700 = {"chmod_d": 0o700} MKD_700 = {"chmod_d": 0o700}
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS) _ = (path, MKD_755, MKD_700)
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"] __all__ = ["path", "MKD_755", "MKD_700"]
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@ -103,40 +99,6 @@ def utime(
return os.utime(fsenc(p), times) return os.utime(fsenc(p), times)
def utime_c(
log: Union["NamedLogger", Any], p: str, ts: int, follow_symlinks: bool = True, throw: bool = False
) -> Optional[int]:
clamp = 0
ov = ts
bp = fsenc(p)
now = int(time.time())
while True:
try:
if SYMTIME:
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
else:
os.utime(bp, (now, ts))
if clamp:
t = "filesystem rejected utime(%r); clamped %s to %s"
log(t % (p, ov, ts))
return ts
except Exception as ex:
pv = ts
while clamp < len(UTIME_CLAMPS):
fun, cv = UTIME_CLAMPS[clamp]
ts = fun(ts, cv)
clamp += 1
if ts != pv:
break
if clamp >= len(UTIME_CLAMPS):
if throw:
raise
else:
t = "could not utime(%r) to %s; %s, %r"
log(t % (p, ov, ex, ex))
return None
if hasattr(os, "lstat"): if hasattr(os, "lstat"):
def lstat(p: str) -> os.stat_result: def lstat(p: str) -> os.stat_result:

View file

@ -12,7 +12,6 @@ import queue
from .__init__ import ANYWIN from .__init__ import ANYWIN
from .authsrv import AuthSrv from .authsrv import AuthSrv
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
from .fsutil import ramdisk_chk
from .httpsrv import HttpSrv from .httpsrv import HttpSrv
from .util import FAKE_MP, Daemon, HMaccas from .util import FAKE_MP, Daemon, HMaccas
@ -57,7 +56,6 @@ class MpWorker(BrokerCli):
# starting to look like a good idea # starting to look like a good idea
self.asrv = AuthSrv(args, None, False) self.asrv = AuthSrv(args, None, False)
ramdisk_chk(self.asrv)
# instantiate all services here (TODO: inheritance?) # instantiate all services here (TODO: inheritance?)
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8) self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
@ -101,7 +99,6 @@ class MpWorker(BrokerCli):
if dest == "reload": if dest == "reload":
self.logw("mpw.asrv reloading") self.logw("mpw.asrv reloading")
self.asrv.reload() self.asrv.reload()
ramdisk_chk(self.asrv)
self.logw("mpw.asrv reloaded") self.logw("mpw.asrv reloaded")
continue continue

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse import argparse
import traceback
from queue import Queue from queue import Queue

View file

@ -130,7 +130,6 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c) nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
names = args.crt_ns.split(",") if args.crt_ns else [] names = args.crt_ns.split(",") if args.crt_ns else []
names = [x.strip() for x in names]
if not args.crt_exact: if not args.crt_exact:
for n in names[:]: for n in names[:]:
names.append("*.{}".format(n)) names.append("*.{}".format(n))

View file

@ -44,7 +44,6 @@ def vf_bmap() -> dict[str, str]:
"gsel", "gsel",
"hardlink", "hardlink",
"magic", "magic",
"md_no_br",
"no_db_ip", "no_db_ip",
"no_sb_md", "no_sb_md",
"no_sb_lg", "no_sb_lg",
@ -52,14 +51,11 @@ def vf_bmap() -> dict[str, str]:
"og", "og",
"og_no_head", "og_no_head",
"og_s_title", "og_s_title",
"opds",
"rand", "rand",
"reflink", "reflink",
"rm_partial",
"rmagic", "rmagic",
"rss", "rss",
"wo_up_readme", "wo_up_readme",
"wram",
"xdev", "xdev",
"xlink", "xlink",
"xvol", "xvol",
@ -84,16 +80,12 @@ def vf_vmap() -> dict[str, str]:
} }
for k in ( for k in (
"bup_ck", "bup_ck",
"casechk",
"chmod_d", "chmod_d",
"chmod_f", "chmod_f",
"dbd", "dbd",
"du_who",
"ufavico",
"forget_ip", "forget_ip",
"hsortn", "hsortn",
"html_head", "html_head",
"html_head_s",
"lg_sbf", "lg_sbf",
"md_sbf", "md_sbf",
"lg_sba", "lg_sba",
@ -110,12 +102,10 @@ def vf_vmap() -> dict[str, str]:
"og_title_i", "og_title_i",
"og_tpl", "og_tpl",
"og_ua", "og_ua",
"opds_exts",
"put_ck", "put_ck",
"put_name", "put_name",
"mv_retry", "mv_retry",
"rm_retry", "rm_retry",
"shr_who",
"sort", "sort",
"tail_fd", "tail_fd",
"tail_rate", "tail_rate",
@ -193,10 +183,8 @@ flagcats = {
"chmod_f=644": "unix-permission for new files", "chmod_f=644": "unix-permission for new files",
"uid=573": "change owner of new files/folders to unix-user 573", "uid=573": "change owner of new files/folders to unix-user 573",
"gid=999": "change owner of new files/folders to unix-group 999", "gid=999": "change owner of new files/folders to unix-group 999",
"wram": "allow uploading into ramdisks",
"sparse": "force use of sparse files, mainly for s3-backed storage", "sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage", "nosparse": "deny use of sparse files, mainly for slow storage",
"rm_partial": "delete unfinished uploads from HDD when they timeout",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
"nosub": "forces all uploads into the top folder of the vfs", "nosub": "forces all uploads into the top folder of the vfs",
"magic": "enables filetype detection for nameless uploads", "magic": "enables filetype detection for nameless uploads",
@ -225,7 +213,6 @@ flagcats = {
"upload rotation\n(moves all uploads into the specified folder structure)": { "upload rotation\n(moves all uploads into the specified folder structure)": {
"rotn=100,3": "3 levels of subfolders with 100 entries in each", "rotn=100,3": "3 levels of subfolders with 100 entries in each",
"rotf=%Y-%m/%d-%H": "date-formatted organizing", "rotf=%Y-%m/%d-%H": "date-formatted organizing",
"rotf_tz=Europe/Oslo": "timezone (default=UTC)",
"lifetime=3600": "uploads are deleted after 1 hour", "lifetime=3600": "uploads are deleted after 1 hour",
}, },
"database, general": { "database, general": {
@ -254,7 +241,6 @@ flagcats = {
"no_db_ip": "never store uploader-IP in the db; disables unpost", "no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs", "fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff", "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
"xlink": "cross-volume dupe detection / linking (dangerous)", "xlink": "cross-volume dupe detection / linking (dangerous)",
"xdev": "do not descend into other filesystems", "xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root", "xvol": "do not follow symlinks leaving the volume root",
@ -305,13 +291,10 @@ flagcats = {
"sort": "default sort order", "sort": "default sort order",
"nsort": "natural-sort of leading digits in filenames", "nsort": "natural-sort of leading digits in filenames",
"hsortn": "number of sort-rules to add to media URLs", "hsortn": "number of sort-rules to add to media URLs",
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
"unlist": "dont list files matching REGEX", "unlist": "dont list files matching REGEX",
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH", "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
"html_head_s=TXT": "additional static text in the html <head>",
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)", "tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
"nodirsz": "don't show total folder size", "nodirsz": "don't show total folder size",
"du_who=all": "show disk-usage info to everyone",
"robots": "allows indexing by search engines (default)", "robots": "allows indexing by search engines (default)",
"norobots": "kindly asks search engines to leave", "norobots": "kindly asks search engines to leave",
"unlistcr": "don't list read-access in controlpanel", "unlistcr": "don't list read-access in controlpanel",
@ -340,12 +323,7 @@ flagcats = {
"og_no_head": "you want to add tags manually with og_tpl", "og_no_head": "you want to add tags manually with og_tpl",
"og_ua": "if defined: only send OG html if useragent matches this regex", "og_ua": "if defined: only send OG html if useragent matches this regex",
}, },
"opds": {
"opds": "enable OPDS",
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
},
"textfiles": { "textfiles": {
"md_no_br": "newline only on double-newline or two tailing spaces",
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope", "md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"exp": "enable textfile expansion; see --help-exp", "exp": "enable textfile expansion; see --help-exp",
"exp_md": "placeholders to expand in markdown files; see --help", "exp_md": "placeholders to expand in markdown files; see --help",
@ -368,7 +346,6 @@ flagcats = {
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
"rss": "allow '?rss' URL suffix (experimental)", "rss": "allow '?rss' URL suffix (experimental)",
"rmagic": "expensive analysis for mimetype accuracy", "rmagic": "expensive analysis for mimetype accuracy",
"shr_who=auth": "who can create shares? no/auth/a",
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
"ups_who=2": "restrict viewing the list of recent uploads", "ups_who=2": "restrict viewing the list of recent uploads",
"zip_who=2": "restrict access to download-as-zip/tar", "zip_who=2": "restrict access to download-as-zip/tar",

View file

@ -7,7 +7,7 @@ import re
import time import time
from .__init__ import ANYWIN, MACOS from .__init__ import ANYWIN, MACOS
from .authsrv import AXS, VFS, AuthSrv from .authsrv import AXS, VFS
from .bos import bos from .bos import bos
from .util import chkcmd, min_ex, undot from .util import chkcmd, min_ex, undot
@ -18,25 +18,22 @@ if True: # pylint: disable=using-constant-test
class Fstab(object): class Fstab(object):
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool): def __init__(self, log: "RootLogger", args: argparse.Namespace):
self.log_func = log self.log_func = log
self.verbose = verbose
self.warned = False self.warned = False
self.trusted = False self.trusted = False
self.tab: Optional[VFS] = None self.tab: Optional[VFS] = None
self.oldtab: Optional[VFS] = None self.oldtab: Optional[VFS] = None
self.srctab = "a" self.srctab = "a"
self.cache: dict[str, tuple[str, str]] = {} self.cache: dict[str, str] = {}
self.age = 0.0 self.age = 0.0
self.maxage = args.mtab_age self.maxage = args.mtab_age
def log(self, msg: str, c: Union[int, str] = 0) -> None: def log(self, msg: str, c: Union[int, str] = 0) -> None:
if not c or self.verbose:
return
self.log_func("fstab", msg, c) self.log_func("fstab", msg, c)
def get(self, path: str) -> tuple[str, str]: def get(self, path: str) -> str:
now = time.time() now = time.time()
if now - self.age > self.maxage or len(self.cache) > 9000: if now - self.age > self.maxage or len(self.cache) > 9000:
self.age = now self.age = now
@ -44,7 +41,6 @@ class Fstab(object):
self.tab = None self.tab = None
self.cache = {} self.cache = {}
mp = ""
fs = "ext4" fs = "ext4"
msg = "failed to determine filesystem at %r; assuming %s\n%s" msg = "failed to determine filesystem at %r; assuming %s\n%s"
@ -54,7 +50,7 @@ class Fstab(object):
path = self._winpath(path) path = self._winpath(path)
except: except:
self.log(msg % (path, fs, min_ex()), 3) self.log(msg % (path, fs, min_ex()), 3)
return fs, "" return fs
path = undot(path) path = undot(path)
try: try:
@ -63,14 +59,14 @@ class Fstab(object):
pass pass
try: try:
fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path) fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
except: except:
self.log(msg % (path, fs, min_ex()), 3) self.log(msg % (path, fs, min_ex()), 3)
fs = fs.lower() fs = fs.lower()
self.cache[path] = (fs, mp) self.cache[path] = fs
self.log("found %s at %r, %r" % (fs, mp, path)) self.log("found %s at %r" % (fs, path))
return fs, mp return fs
def _winpath(self, path: str) -> str: def _winpath(self, path: str) -> str:
# try to combine volume-label + st_dev (vsn) # try to combine volume-label + st_dev (vsn)
@ -85,49 +81,34 @@ class Fstab(object):
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {}) self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
self.trusted = False self.trusted = False
def _from_sp_mount(self) -> dict[str, str]: def build_tab(self) -> None:
self.log("inspecting mtab for changes")
sptn = r"^.*? on (.*) type ([^ ]+) \(.*" sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
if MACOS: if MACOS:
sptn = r"^.*? on (.*) \(([^ ]+), .*" sptn = r"^.*? on (.*) \(([^ ]+), .*"
ptn = re.compile(sptn) ptn = re.compile(sptn)
so, _ = chkcmd(["mount"]) so, _ = chkcmd(["mount"])
dtab: dict[str, str] = {} tab1: list[tuple[str, str]] = []
atab = []
for ln in so.split("\n"): for ln in so.split("\n"):
m = ptn.match(ln) m = ptn.match(ln)
if not m: if not m:
continue continue
zs1, zs2 = m.groups() zs1, zs2 = m.groups()
dtab[str(zs1)] = str(zs2) tab1.append((str(zs1), str(zs2)))
atab.append(ln)
return dtab
def _from_proc(self) -> dict[str, str]:
ret: dict[str, str] = {}
with open("/proc/self/mounts", "rb", 262144) as f:
src = f.read(262144).decode("utf-8", "replace").split("\n")
for zsl in [x.split(" ") for x in src]:
if len(zsl) < 3:
continue
zs = zsl[1]
zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
ret[zs] = zsl[2]
return ret
def build_tab(self) -> None:
self.log("inspecting mtab for changes")
dtab = self._from_sp_mount() if MACOS else self._from_proc()
# keep empirically-correct values if mounttab unchanged # keep empirically-correct values if mounttab unchanged
srctab = str(sorted(dtab.items())) srctab = "\n".join(sorted(atab))
if srctab == self.srctab: if srctab == self.srctab:
self.tab = self.oldtab self.tab = self.oldtab
return return
self.log("mtab has changed; reevaluating support for sparse files") self.log("mtab has changed; reevaluating support for sparse files")
tab1 = list(dtab.items())
tab1.sort(key=lambda x: (len(x[0]), x[0])) tab1.sort(key=lambda x: (len(x[0]), x[0]))
path1, fs1 = tab1[0] path1, fs1 = tab1[0]
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {}) tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
@ -165,7 +146,7 @@ class Fstab(object):
vn.realpath = ptn.sub(nval, vn.realpath) vn.realpath = ptn.sub(nval, vn.realpath)
visit.extend(list(vn.nodes.values())) visit.extend(list(vn.nodes.values()))
def get_unix(self, path: str) -> tuple[str, str]: def get_unix(self, path: str) -> str:
if not self.tab: if not self.tab:
try: try:
self.build_tab() self.build_tab()
@ -174,44 +155,20 @@ class Fstab(object):
# prisonparty or other restrictive environment # prisonparty or other restrictive environment
if not self.warned: if not self.warned:
self.warned = True self.warned = True
t = "failed to associate fs-mounts with the VFS (this is fine):\n%s" self.log("failed to build tab:\n{}".format(min_ex()), 3)
self.log(t % (min_ex(),), 6)
self.build_fallback() self.build_fallback()
assert self.tab # !rm assert self.tab # !rm
ret = self.tab._find(path)[0] ret = self.tab._find(path)[0]
if self.trusted or path == ret.vpath: if self.trusted or path == ret.vpath:
return ret.realpath.split("/")[0], ret.vpath return ret.realpath.split("/")[0]
else: else:
return "idk", "" return "idk"
def get_w32(self, path: str) -> tuple[str, str]: def get_w32(self, path: str) -> str:
if not self.tab: if not self.tab:
self.build_fallback() self.build_fallback()
assert self.tab # !rm assert self.tab # !rm
ret = self.tab._find(path)[0] ret = self.tab._find(path)[0]
return ret.realpath, "" return ret.realpath
def ramdisk_chk(asrv: AuthSrv) -> None:
# should have been in authsrv but that's a circular import
mods = []
ramfs = ("tmpfs", "overlay")
log = asrv.log_func or print
fstab = Fstab(log, asrv.args, False)
for vn in asrv.vfs.all_nodes.values():
if not vn.axs.uwrite or "wram" in vn.flags:
continue
ap = vn.realpath
if not ap or os.path.isfile(ap):
continue
fs, mp = fstab.get(ap)
mp = "/" + mp.strip("/")
if fs == "tmpfs" or (mp == "/" and fs in ramfs):
mods.append((vn.vpath, ap, fs, mp))
vn.axs.uwrite.clear()
if mods:
t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
log("vfs", t + "".join(t2) + "\n", 1)

View file

@ -68,13 +68,13 @@ class FtpAuth(DummyAuthorizer):
if ip.startswith("::ffff:"): if ip.startswith("::ffff:"):
ip = ip[7:] ip = ip[7:]
ipn = ipnorm(ip) ip = ipnorm(ip)
bans = self.hub.bans bans = self.hub.bans
if ipn in bans: if ip in bans:
rt = bans[ipn] - time.time() rt = bans[ip] - time.time()
if rt < 0: if rt < 0:
logging.info("client unbanned") logging.info("client unbanned")
del bans[ipn] del bans[ip]
else: else:
raise AuthenticationFailed("banned") raise AuthenticationFailed("banned")
@ -152,6 +152,10 @@ class FtpFs(AbstractedFS):
self.cwd = "/" # pyftpdlib convention of leading slash self.cwd = "/" # pyftpdlib convention of leading slash
self.root = "/var/lib/empty" self.root = "/var/lib/empty"
self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False
self.can_admin = self.can_dot = False
self.listdirinfo = self.listdir self.listdirinfo = self.listdir
self.chdir(".") self.chdir(".")
@ -202,9 +206,6 @@ class FtpFs(AbstractedFS):
if r and not cr or w and not cw or m and not cm or d and not cd: 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) raise FSE(t.format(vpath), 1)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise FSE("No such file or directory", 1)
return os.path.join(vfs.realpath, rem), vfs, rem return os.path.join(vfs.realpath, rem), vfs, rem
except Pebkac as ex: except Pebkac as ex:
raise FSE(str(ex)) raise FSE(str(ex))
@ -217,7 +218,7 @@ class FtpFs(AbstractedFS):
m: bool = False, m: bool = False,
d: bool = False, d: bool = False,
) -> tuple[str, VFS, str]: ) -> tuple[str, VFS, str]:
return self.v2a(join(self.cwd, vpath), r, w, m, d) return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
def ftp2fs(self, ftppath: str) -> str: def ftp2fs(self, ftppath: str) -> str:
# return self.v2a(ftppath) # return self.v2a(ftppath)
@ -279,10 +280,6 @@ class FtpFs(AbstractedFS):
def chdir(self, path: str) -> None: def chdir(self, path: str) -> None:
nwd = join(self.cwd, path) nwd = join(self.cwd, path)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False) vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
if not vfs.realpath:
self.cwd = nwd
return
ap = vfs.canonical(rem) ap = vfs.canonical(rem)
try: try:
st = bos.stat(ap) st = bos.stat(ap)
@ -292,11 +289,24 @@ class FtpFs(AbstractedFS):
# returning 550 is library-default and suitable # returning 550 is library-default and suitable
raise FSE("No such file or directory") raise FSE("No such file or directory")
if vfs.realpath:
avfs = vfs.chk_ap(ap, st) avfs = vfs.chk_ap(ap, st)
if not avfs: if not avfs:
raise FSE("Permission denied", 1) raise FSE("Permission denied", 1)
else:
avfs = vfs
self.cwd = nwd self.cwd = nwd
(
self.can_read,
self.can_write,
self.can_move,
self.can_delete,
self.can_get,
self.can_upget,
self.can_admin,
self.can_dot,
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None: def mkdir(self, path: str) -> None:
ap, vfs, _ = self.rv2a(path, w=True) ap, vfs, _ = self.rv2a(path, w=True)
@ -319,7 +329,7 @@ class FtpFs(AbstractedFS):
vfs_ls = [x[0] for x in vfs_ls1] vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys()) vfs_ls.extend(vfs_virt.keys())
if self.uname not in vfs.axs.udot: if not self.can_dot:
vfs_ls = exclude_dotfiles(vfs_ls) vfs_ls = exclude_dotfiles(vfs_ls)
vfs_ls.sort() vfs_ls.sort()
@ -367,13 +377,16 @@ class FtpFs(AbstractedFS):
raise FSE(str(ex)) raise FSE(str(ex))
def rename(self, src: str, dst: str) -> None: def rename(self, src: str, dst: str) -> None:
if not self.can_move:
raise FSE("Not allowed for user " + self.h.uname)
if self.args.no_mv: if self.args.no_mv:
raise FSE("The rename/move feature is disabled in server config") raise FSE("The rename/move feature is disabled in server config")
svp = join(self.cwd, src).lstrip("/") svp = join(self.cwd, src).lstrip("/")
dvp = join(self.cwd, dst).lstrip("/") dvp = join(self.cwd, dst).lstrip("/")
try: try:
self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp) self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
except Exception as ex: except Exception as ex:
raise FSE(str(ex)) raise FSE(str(ex))
@ -396,8 +409,12 @@ class FtpFs(AbstractedFS):
return st return st
def utime(self, path: str, timeval: float) -> None: def utime(self, path: str, timeval: float) -> None:
try:
ap = self.rv2a(path, w=True)[0] ap = self.rv2a(path, w=True)[0]
bos.utime_c(logging.warning, ap, int(timeval), False) return bos.utime(ap, (int(time.time()), int(timeval)))
except Exception as ex:
logging.error("ftp.utime: %s, %r", ex, ex)
raise
def lstat(self, path: str) -> os.stat_result: def lstat(self, path: str) -> os.stat_result:
ap = self.rv2a(path)[0] ap = self.rv2a(path)[0]
@ -493,8 +510,7 @@ class FtpHandler(FTPHandler):
return return
self.vfs_map[ap] = vp self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu") xbu = vfs.flags.get("xbu")
if xbu: if xbu and not runhook(
hr = runhook(
None, None,
None, None,
self.hub.up2k, self.hub.up2k,
@ -510,13 +526,8 @@ class FtpHandler(FTPHandler):
self.cli_ip, self.cli_ip,
time.time(), time.time(),
"", "",
) ):
t = hr.get("rejectmsg") or "" raise FSE("Upload blocked by xbu server config")
if t or not hr:
if not t:
t = "Upload blocked by xbu server config: %r" % (vp,)
self.respond("550 %s" % (t,), logging.info)
return
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap)) # print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
ret = FTPHandler.ftp_STOR(self, file, mode) ret = FTPHandler.ftp_STOR(self, file, mode)

File diff suppressed because it is too large Load diff

View file

@ -187,7 +187,6 @@ class HttpSrv(object):
"svcs", "svcs",
] ]
self.j2 = {x: env.get_template(x + ".html") for x in jn} self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.j2["opds"] = env.get_template("opds.xml")
self.prism = has_resource(self.E, "web/deps/prism.js.gz") self.prism = has_resource(self.E, "web/deps/prism.js.gz")
if self.args.ipu: if self.args.ipu:
@ -572,7 +571,7 @@ class HttpSrv(object):
v = self.E.t0 v = self.E.t0
try: try:
with os.scandir(self.E.mod_ + "web") as dh: with os.scandir(os.path.join(self.E.mod, "web")) as dh:
for fh in dh: for fh in dh:
inf = fh.stat() inf = fh.stat()
v = max(v, inf.st_mtime) v = max(v, inf.st_mtime)

View file

@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import errno import errno
import os
import random import random
import select import select
import socket import socket
@ -13,14 +12,7 @@ from ipaddress import IPv4Network, IPv6Network
from .__init__ import TYPE_CHECKING from .__init__ import TYPE_CHECKING
from .__init__ import unicode as U from .__init__ import unicode as U
from .multicast import MC_Sck, MCast from .multicast import MC_Sck, MCast
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex from .stolen.dnslib import AAAA
try:
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_DNSLIB"):
raise ImportError()
from .stolen.dnslib import (
AAAA,
)
from .stolen.dnslib import CLASS as DC from .stolen.dnslib import CLASS as DC
from .stolen.dnslib import ( from .stolen.dnslib import (
NSEC, NSEC,
@ -35,30 +27,7 @@ try:
DNSRecord, DNSRecord,
set_avahi_379, set_avahi_379,
) )
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
DNS_VND = True
except ImportError:
DNS_VND = False
from dnslib import (
AAAA,
)
from dnslib import CLASS as DC
from dnslib import (
NSEC,
PTR,
QTYPE,
RR,
SRV,
TXT,
A,
Bimap,
DNSHeader,
DNSQuestion,
DNSRecord,
)
DC.forward[0x8001] = "F_IN"
DC.reverse["F_IN"] = 0x8001
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
@ -66,11 +35,6 @@ if TYPE_CHECKING:
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union from typing import Any, Optional, Union
if os.getenv("PRTY_MODSPEC"):
from inspect import getsourcefile
print("PRTY_MODSPEC: dnslib:", getsourcefile(A))
MDNS4 = "224.0.0.251" MDNS4 = "224.0.0.251"
MDNS6 = "ff02::fb" MDNS6 = "ff02::fb"
@ -109,7 +73,7 @@ class MDNS(MCast):
self.ngen = ngen self.ngen = ngen
self.ttl = 300 self.ttl = 300
if not self.args.zm_nwa_1 and DNS_VND: if not self.args.zm_nwa_1:
set_avahi_379() set_avahi_379()
zs = self.args.zm_fqdn or (self.args.name + ".local") zs = self.args.zm_fqdn or (self.args.name + ".local")
@ -136,14 +100,9 @@ class MDNS(MCast):
self.log_func(self.logsrc, msg, c) self.log_func(self.logsrc, msg, c)
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]: def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
ar = self.args
zms = self.args.zms zms = self.args.zms
http = {"port": 80 if 80 in self.args.p else self.args.p[0]}
zi = ar.zm_http https = {"port": 443 if 443 in self.args.p else self.args.p[0]}
http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}
zi = ar.zm_https
https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}
webdav = http.copy() webdav = http.copy()
webdavs = https.copy() webdavs = https.copy()
webdav["u"] = webdavs["u"] = "u" # KDE requires username webdav["u"] = webdavs["u"] = "u" # KDE requires username
@ -168,16 +127,16 @@ class MDNS(MCast):
svcs: dict[str, dict[str, Any]] = {} svcs: dict[str, dict[str, Any]] = {}
if "d" in zms and http["port"]: if "d" in zms:
svcs["_webdav._tcp.local."] = webdav svcs["_webdav._tcp.local."] = webdav
if "D" in zms and https["port"]: if "D" in zms:
svcs["_webdavs._tcp.local."] = webdavs svcs["_webdavs._tcp.local."] = webdavs
if "h" in zms and http["port"]: if "h" in zms:
svcs["_http._tcp.local."] = http svcs["_http._tcp.local."] = http
if "H" in zms and https["port"]: if "H" in zms:
svcs["_https._tcp.local."] = https svcs["_https._tcp.local."] = https
if "f" in zms.lower(): if "f" in zms.lower():
@ -416,7 +375,7 @@ class MDNS(MCast):
cip = addr[0] cip = addr[0]
v6 = ":" in cip v6 = ":" in cip
if (cip.startswith("169.254") and not self.ll_ok) or ( if (cip.startswith("169.254") and not self.ll_ok) or (
v6 and not cip.startswith(IP6_LL) v6 and not cip.startswith("fe80")
): ):
return return

View file

@ -178,7 +178,6 @@ def au_unpk(
elif pk == "epub": elif pk == "epub":
fi = get_cover_from_epub(log, abspath) fi = get_cover_from_epub(log, abspath)
assert fi # !rm
else: else:
raise Exception("unknown compression %s" % (pk,)) raise Exception("unknown compression %s" % (pk,))
@ -200,10 +199,9 @@ def au_unpk(
except Exception as ex: except Exception as ex:
if ret: if ret:
t = "failed to decompress file %r: %r" t = "failed to decompress audio file %r: %r"
log(t % (abspath, ex)) log(t % (abspath, ex))
wunlink(log, ret, vn.flags if vn else VF_CAREFUL) wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
return ""
return abspath return abspath
@ -423,17 +421,10 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
# This might be an EPUB2 file, try the legacy way of specifying covers # This might be an EPUB2 file, try the legacy way of specifying covers
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns) coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
if not coverimage_path:
raise Exception("no cover inside epub")
# This url is either absolute (in the .epub) or relative to the package document # This url is either absolute (in the .epub) or relative to the package document
adjusted_cover_path = urljoin(rootfile_path, coverimage_path) adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
try:
return z.open(adjusted_cover_path) return z.open(adjusted_cover_path)
except KeyError:
t = "epub: cover specified in package document, but doesn't exist: %s"
log(t % (adjusted_cover_path,))
def _get_cover_from_epub2( def _get_cover_from_epub2(
@ -441,8 +432,9 @@ def _get_cover_from_epub2(
) -> Optional[str]: ) -> Optional[str]:
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then # <meta name="cover" content="id-to-cover-image"> in <metadata>, then
# <item> in <manifest> # <item> in <manifest>
xn = package_root.find("./metadata/meta[@name='cover']", package_ns) cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
cover_id = xn.get("content") if xn is not None else None "content"
)
if not cover_id: if not cover_id:
return None return None
@ -518,6 +510,7 @@ class MTag(object):
"album-artist", "album-artist",
"tpe2", "tpe2",
"aart", "aart",
"conductor",
"organization", "organization",
"band", "band",
], ],
@ -651,9 +644,6 @@ class MTag(object):
return self._get(abspath) return self._get(abspath)
ap = au_unpk(self.log, self.args.au_unpk, abspath) ap = au_unpk(self.log, self.args.au_unpk, abspath)
if not ap:
return {}
ret = self._get(ap) ret = self._get(ap)
if ap != abspath: if ap != abspath:
wunlink(self.log, ap, VF_CAREFUL) wunlink(self.log, ap, VF_CAREFUL)
@ -759,9 +749,6 @@ class MTag(object):
ap = abspath ap = abspath
ret: dict[str, Any] = {} ret: dict[str, Any] = {}
if not ap:
return ret
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])): for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
try: try:
cmd = [parser.bin, ap] cmd = [parser.bin, ap]

View file

@ -15,7 +15,7 @@ from ipaddress import (
) )
from .__init__ import MACOS, TYPE_CHECKING from .__init__ import MACOS, TYPE_CHECKING
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack from .util import Daemon, Netdev, find_prefix, min_ex, spack
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
@ -96,10 +96,7 @@ class MCast(object):
def create_servers(self) -> list[str]: def create_servers(self) -> list[str]:
bound: list[str] = [] bound: list[str] = []
netdevs = self.hub.tcpsrv.netdevs netdevs = self.hub.tcpsrv.netdevs
blist = self.hub.tcpsrv.bound ips = [x[0] for x in self.hub.tcpsrv.bound]
if self.args.http_no_tcp:
blist = self.hub.tcpsrv.seen_eps
ips = [x[0] for x in blist]
if "::" in ips: if "::" in ips:
ips = [x for x in ips if x != "::"] + list( ips = [x for x in ips if x != "::"] + list(
@ -148,7 +145,7 @@ class MCast(object):
all_selected = ips[:] all_selected = ips[:]
# discard non-linklocal ipv6 # discard non-linklocal ipv6
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)] ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
if not ips: if not ips:
raise NoIPs() raise NoIPs()
@ -186,7 +183,7 @@ class MCast(object):
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False) srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
# gvfs breaks if a linklocal ip appears in a dns reply # gvfs breaks if a linklocal ip appears in a dns reply
ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)} ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
rt = {k: v for k, v in srv.ips.items() if k not in ll} rt = {k: v for k, v in srv.ips.items() if k not in ll}
if self.args.ll or not rt: if self.args.ll or not rt:

View file

@ -25,7 +25,6 @@ class PWHash(object):
self.args = args self.args = args
zsl = args.ah_alg.split(",") zsl = args.ah_alg.split(",")
zsl = [x.strip() for x in zsl]
alg = zsl[0] alg = zsl[0]
if alg == "none": if alg == "none":
alg = "" alg = ""

View file

@ -1,112 +0,0 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
try:
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_QRCG"):
raise ImportError()
from .stolen.qrcodegen import QrCode
qrgen = QrCode.encode_binary
VENDORED = True
except ImportError:
VENDORED = False
from qrcodegen import QrCode
if os.getenv("PRTY_MODSPEC"):
from inspect import getsourcefile
print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode))
if True: # pylint: disable=using-constant-test
import typing
from typing import Any, Optional, Sequence, Union
if not VENDORED:
def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode":
ret = None
V = QrCode.Ecc
for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:
qr = QrCode.encode_binary(data, e)
qr.size = qr._size
qr.modules = qr._modules
if not ret or ret.size > qr.size:
ret = qr
return ret
qrgen = _qrgen
def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:
tab = qr.modules
sz = qr.size
if sz % 2 and zoom == 1:
tab.append([False] * sz)
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
tab = [[False] * pad + x + [False] * pad for x in tab]
rows: list[str] = []
if zoom == 1:
for y in range(0, len(tab), 2):
row = ""
for x in range(len(tab[y])):
v = 2 if tab[y][x] else 0
v += 1 if tab[y + 1][x] else 0
row += " ▄▀█"[v]
rows.append(row)
else:
for tr in tab:
row = ""
for zb in tr:
row += ""[int(zb)] * 2
rows.append(row)
return "\n".join(rows)
def qr2png(
qr: QrCode,
zoom: int,
pad: int,
bg: Optional[tuple[int, int, int]],
fg: Optional[tuple[int, int, int]],
ap: str,
) -> None:
from PIL import Image
tab = qr.modules
sz = qr.size
psz = sz + pad * 2
if bg:
img = Image.new("RGB", (psz, psz), bg)
else:
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
fg = (fg[0], fg[1], fg[2], 255)
for y in range(sz):
for x in range(sz):
if tab[y][x]:
img.putpixel((x + pad, y + pad), fg)
if zoom != 1:
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
img.save(ap)
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

View file

@ -246,8 +246,7 @@ class SMB(object):
ap = absreal(ap) ap = absreal(ap)
xbu = vfs.flags.get("xbu") xbu = vfs.flags.get("xbu")
if xbu: if xbu and not runhook(
hr = runhook(
self.nlog, self.nlog,
None, None,
self.hub.up2k, self.hub.up2k,
@ -263,12 +262,8 @@ class SMB(object):
"1.7.6.2", "1.7.6.2",
time.time(), time.time(),
"", "",
) ):
t = hr.get("rejectmsg") or "" yeet("blocked by xbu server config: %r" % (vpath,))
if t or not hr:
if not t:
t = "blocked by xbu server config: %r" % (vpath,)
yeet(t)
ret = bos.open(ap, flags, *a, mode=chmod, **ka) ret = bos.open(ap, flags, *a, mode=chmod, **ka)
if wr: if wr:
@ -323,7 +318,7 @@ class SMB(object):
t = "blocked rename (no-move-acc %s): /%s @%s" t = "blocked rename (no-move-acc %s): /%s @%s"
yeet(t % (vfs1.axs.umove, vp1, uname)) yeet(t % (vfs1.axs.umove, vp1, uname))
self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2) self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
try: try:
bos.makedirs(ap2, vf=vfs2.flags) bos.makedirs(ap2, vf=vfs2.flags)
except: except:
@ -378,7 +373,7 @@ class SMB(object):
t = "blocked utime (no-write-acc %s): /%s @%s" t = "blocked utime (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname)) yeet(t % (vfs.axs.uwrite, vpath, uname))
bos.utime_c(info, ap, int(times[1]), False) return bos.utime(ap, times)
def _p_exists(self, vpath: str) -> bool: def _p_exists(self, vpath: str) -> bool:
# ap = "?" # ap = "?"

View file

@ -4,7 +4,7 @@
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py # https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
# the original ^ is extremely well commented so refer to that for explanations # the original ^ is extremely well commented so refer to that for explanations
# hacks: binary-only, auto-ecc, py2-compat # hacks: binary-only, auto-ecc, render, py2-compat
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
@ -173,6 +173,33 @@ class QrCode(object):
self._apply_mask(msk) # Apply the final choice of mask self._apply_mask(msk) # Apply the final choice of mask
self._draw_format_bits(msk) # Overwrite old format bits self._draw_format_bits(msk) # Overwrite old format bits
def render(self, zoom=1, pad=4) -> str:
tab = self.modules
sz = self.size
if sz % 2 and zoom == 1:
tab.append([False] * sz)
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
tab = [[False] * pad + x + [False] * pad for x in tab]
rows: list[str] = []
if zoom == 1:
for y in range(0, len(tab), 2):
row = ""
for x in range(len(tab[y])):
v = 2 if tab[y][x] else 0
v += 1 if tab[y + 1][x] else 0
row += " ▄▀█"[v]
rows.append(row)
else:
for tr in tab:
row = ""
for zb in tr:
row += ""[int(zb)] * 2
rows.append(row)
return "\n".join(rows)
def _draw_function_patterns(self) -> None: def _draw_function_patterns(self) -> None:
# Draw horizontal and vertical timing patterns # Draw horizontal and vertical timing patterns
for i in range(self.size): for i in range(self.size):
@ -567,3 +594,20 @@ def _get_bit(x: int, i: int) -> bool:
class DataTooLongError(ValueError): class DataTooLongError(ValueError):
pass pass
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

View file

@ -27,10 +27,9 @@ if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union from typing import Any, Optional, Union
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who from .authsrv import BAD_CFG, AuthSrv
from .bos import bos from .bos import bos
from .cert import ensure_cert from .cert import ensure_cert
from .fsutil import ramdisk_chk
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2 from .pwhash import HAVE_ARGON2
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
@ -67,7 +66,6 @@ from .util import (
build_netmap, build_netmap,
expat_ver, expat_ver,
gzip, gzip,
html_escape,
load_ipr, load_ipr,
load_ipu, load_ipu,
lock_file, lock_file,
@ -136,7 +134,6 @@ class SvcHub(object):
self.nsigs = 3 self.nsigs = 3
self.retcode = 0 self.retcode = 0
self.httpsrv_up = 0 self.httpsrv_up = 0
self.qr_tsz = None
self.log_mutex = threading.Lock() self.log_mutex = threading.Lock()
self.cday = 0 self.cday = 0
@ -158,7 +155,7 @@ class SvcHub(object):
args.unpost = 0 args.unpost = 0
args.no_del = True args.no_del = True
args.no_mv = True args.no_mv = True
args.reflink = True args.hardlink = True
args.dav_auth = True args.dav_auth = True
args.vague_403 = True args.vague_403 = True
args.nih = True args.nih = True
@ -248,8 +245,8 @@ class SvcHub(object):
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s" t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
self.log("root", t % (args.th_ram_max, zs), 3) self.log("root", t % (args.th_ram_max, zs), 3)
if args.chpw and args.have_idp_hdrs and "pw" not in args.auth_ord.split(","): if args.chpw and args.have_idp_hdrs:
t = "ERROR: user-changeable passwords is not compatible with your current configuration. Choose one of these options to fix it:\n option1: disable --chpw\n option2: remove all use of IdP features; --idp-*\n option3: change --auth-ord to something like pw,idp,ipu" t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1) self.log("root", t, 1)
raise Exception(t) raise Exception(t)
@ -291,14 +288,6 @@ class SvcHub(object):
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)] ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
args.theme = "{0}{1} {0} {1}".format(ch, bri) args.theme = "{0}{1} {0} {1}".format(ch, bri)
if args.nid:
args.du_who = "no"
args.du_iwho = n_du_who(args.du_who)
if args.ver and args.ver_who == "no":
args.ver_who = "all"
args.ver_iwho = n_ver_who(args.ver_who)
if args.nih: if args.nih:
args.vname = "" args.vname = ""
args.doctitle = args.doctitle.replace(" @ --name", "") args.doctitle = args.doctitle.replace(" @ --name", "")
@ -312,7 +301,6 @@ class SvcHub(object):
# initiate all services to manage # initiate all services to manage
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs) self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
ramdisk_chk(self.asrv)
if args.cgen: if args.cgen:
self.asrv.cgen() self.asrv.cgen()
@ -337,7 +325,7 @@ class SvcHub(object):
self._feature_test() self._feature_test()
decs = {k.strip(): 1 for k in self.args.th_dec.split(",")} decs = {k: 1 for k in self.args.th_dec.split(",")}
if not HAVE_VIPS: if not HAVE_VIPS:
decs.pop("vips", None) decs.pop("vips", None)
if not HAVE_PIL: if not HAVE_PIL:
@ -392,10 +380,7 @@ class SvcHub(object):
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]" t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
raise Exception(t % (args.q_mp3,)) raise Exception(t % (args.q_mp3,))
else: else:
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(",")) args.au_unpk = {}
args.au_unpk = {
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
}
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage) args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
@ -448,7 +433,7 @@ class SvcHub(object):
# create netmaps early to avoid firewall gaps, # create netmaps early to avoid firewall gaps,
# but the mutex blocks multiprocessing startup # but the mutex blocks multiprocessing startup
for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split(): for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
try: try:
getattr(args, zs).mutex = threading.Lock() getattr(args, zs).mutex = threading.Lock()
except: except:
@ -802,27 +787,7 @@ class SvcHub(object):
self.signal_handler(signal.SIGTERM, None) self.signal_handler(signal.SIGTERM, None)
def sticky_qr(self) -> None: def sticky_qr(self) -> None:
self._sticky_qr() tw, th = termsize()
def _unsticky_qr(self, flush=True) -> None:
print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
if flush:
sys.stderr.flush()
def _sticky_qr(self, force: bool = False) -> None:
sz = termsize()
if self.qr_tsz == sz:
if not force:
return
else:
force = False
if self.qr_tsz:
self._unsticky_qr(False)
else:
atexit.register(self._unsticky_qr)
tw, th = self.qr_tsz = sz
zs1, qr = self.tcpsrv.qr.split("\n", 1) zs1, qr = self.tcpsrv.qr.split("\n", 1)
url, colr = zs1.split(" ", 1) url, colr = zs1.split(" ", 1)
nl = len(qr.split("\n")) # numlines nl = len(qr.split("\n")) # numlines
@ -846,38 +811,13 @@ class SvcHub(object):
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url) url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
qr = colr + qr qr = colr + qr
def unlock():
print("\033[s\033[r\033[u", file=sys.stderr)
atexit.register(unlock)
t = "%s\033[%dA" % ("\n" * nl, nl) t = "%s\033[%dA" % ("\n" * nl, nl)
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url) t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
if not force: self.pr(t, file=sys.stderr)
self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
self.pr(t, file=sys.stderr, end="")
def _qr_thr(self):
qr = self.tcpsrv.qr
w8 = self.args.qr_wait
if w8:
time.sleep(w8)
self.log("qr-code", qr)
if self.args.qr_stdout:
self.pr(self.tcpsrv.qr)
if self.args.qr_stderr:
self.pr(self.tcpsrv.qr, file=sys.stderr)
w8 = self.args.qr_every
msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
while w8:
time.sleep(w8)
if self.stopping:
break
if self.args.qr_pin:
self._sticky_qr(True)
else:
self.log("qr-code", msg)
w8 = self.args.qr_winch
while w8:
time.sleep(w8)
if self.stopping:
break
self._sticky_qr()
def cb_httpsrv_up(self) -> None: def cb_httpsrv_up(self) -> None:
self.httpsrv_up += 1 self.httpsrv_up += 1
@ -893,15 +833,8 @@ class SvcHub(object):
if self.tcpsrv.qr: if self.tcpsrv.qr:
if self.args.qr_pin: if self.args.qr_pin:
self.sticky_qr() self.sticky_qr()
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
Daemon(self._qr_thr, "qr")
else: else:
if not self.args.qr_pin:
self.log("qr-code", self.tcpsrv.qr) self.log("qr-code", self.tcpsrv.qr)
if self.args.qr_stdout:
self.pr(self.tcpsrv.qr)
if self.args.qr_stderr:
self.pr(self.tcpsrv.qr, file=sys.stderr)
else: else:
self.log("root", "workers OK\n") self.log("root", "workers OK\n")
@ -999,24 +932,6 @@ class SvcHub(object):
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n" t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1) self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
if not al.no_ses:
al.no_ses = True
t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
if al.idp_store == 1:
al.idp_store = 0
self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
if al.idp_store:
t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
self.log("root", t2, 1)
raise Exception(t2)
if al.shr:
t2 = "ERROR: Cannot enable shares %s" % (t,)
self.log("root", t2, 1)
raise Exception(t2)
def _process_config(self) -> bool: def _process_config(self) -> bool:
al = self.args al = self.args
@ -1172,15 +1087,8 @@ class SvcHub(object):
if len(al.tcolor) == 3: # fc5 => ffcc55 if len(al.tcolor) == 3: # fc5 => ffcc55
al.tcolor = "".join([x * 2 for x in al.tcolor]) al.tcolor = "".join([x * 2 for x in al.tcolor])
if self.args.name_url:
zs = html_escape(self.args.name_url, True, True)
zs = '<a href="%s">%s</a>' % (zs, self.args.name)
else:
zs = self.args.name
self.args.name_html = zs
zs = al.u2sz zs = al.u2sz
zsl = [x.strip() for x in zs.split(",")] zsl = zs.split(",")
if len(zsl) not in (1, 3): if len(zsl) not in (1, 3):
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)" t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
raise Exception(t) raise Exception(t)
@ -1197,7 +1105,6 @@ class SvcHub(object):
zi2 = zi zi2 = zi
al.u2sz = ",".join(zsl) al.u2sz = ",".join(zsl)
derive_args(al)
return True return True
def _ipa2re(self, txt) -> Optional[re.Pattern]: def _ipa2re(self, txt) -> Optional[re.Pattern]:
@ -1373,7 +1280,6 @@ class SvcHub(object):
with self.reload_mutex: with self.reload_mutex:
self.log("root", "reloading config") self.log("root", "reloading config")
self.asrv.reload(9 if up2k else 4) self.asrv.reload(9 if up2k else 4)
ramdisk_chk(self.asrv)
if up2k: if up2k:
self.up2k.reload(rescan_all_vols) self.up2k.reload(rescan_all_vols)
t += "; volumes are now reinitializing" t += "; volumes are now reinitializing"

View file

@ -9,19 +9,17 @@ import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .cert import gencert from .cert import gencert
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen from .stolen.qrcodegen import QrCode
from .util import ( from .util import (
E_ACCESS, E_ACCESS,
E_ADDR_IN_USE, E_ADDR_IN_USE,
E_ADDR_NOT_AVAIL, E_ADDR_NOT_AVAIL,
E_UNREACH, E_UNREACH,
HAVE_IPV6, HAVE_IPV6,
IP6_LL,
IP6ALL, IP6ALL,
VF_CAREFUL, VF_CAREFUL,
Netdev, Netdev,
atomic_move, atomic_move,
get_adapters,
min_ex, min_ex,
sunpack, sunpack,
termsize, termsize,
@ -60,7 +58,6 @@ class TcpSrv(object):
self.stopping = False self.stopping = False
self.srv: list[socket.socket] = [] self.srv: list[socket.socket] = []
self.bound: list[tuple[str, int]] = [] self.bound: list[tuple[str, int]] = []
self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only
self.netdevs: dict[str, Netdev] = {} self.netdevs: dict[str, Netdev] = {}
self.netlist = "" self.netlist = ""
self.nsrv = 0 self.nsrv = 0
@ -143,12 +140,12 @@ class TcpSrv(object):
# keep IPv6 LL-only nics # keep IPv6 LL-only nics
ll_ok: set[str] = set() ll_ok: set[str] = set()
for ip, nd in self.netdevs.items(): for ip, nd in self.netdevs.items():
if not ip.startswith(IP6_LL): if not ip.startswith("fe80"):
continue continue
just_ll = True just_ll = True
for ip2, nd2 in self.netdevs.items(): for ip2, nd2 in self.netdevs.items():
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL): if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
just_ll = False just_ll = False
if just_ll or self.args.ll: if just_ll or self.args.ll:
@ -167,7 +164,7 @@ class TcpSrv(object):
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")] title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)" t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
for ip, desc in sorted(eps.items(), key=lambda x: x[1]): for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
if ip.startswith(IP6_LL) and ip not in ll_ok: if ip.startswith("fe80") and ip not in ll_ok:
continue continue
for port in sorted(self.args.p): for port in sorted(self.args.p):
@ -301,19 +298,12 @@ class TcpSrv(object):
try: try:
if tcp: if tcp:
if self.args.http_no_tcp:
self.seen_eps.append((ip, port))
return
srv.bind((ip, port)) srv.bind((ip, port))
else: else:
if ANYWIN or self.args.rm_sck: if ANYWIN or self.args.rm_sck:
if os.path.exists(ip): if os.path.exists(ip):
os.unlink(ip) os.unlink(ip)
srv.bind(ip) srv.bind(ip)
if uds_gid != -1:
os.chown(ip, -1, uds_gid)
if uds_perm != -1:
os.chmod(ip, uds_perm)
else: else:
tf = "%s.%d" % (ip, os.getpid()) tf = "%s.%d" % (ip, os.getpid())
if os.path.exists(tf): if os.path.exists(tf):
@ -414,7 +404,6 @@ class TcpSrv(object):
self.srv = srvs self.srv = srvs
self.bound = bound self.bound = bound
self.seen_eps = list(set(self.seen_eps + bound))
self.nsrv = len(srvs) self.nsrv = len(srvs)
self._distribute_netdevs() self._distribute_netdevs()
@ -457,6 +446,8 @@ class TcpSrv(object):
self._distribute_netdevs() self._distribute_netdevs()
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
from .stolen.ifaddr import get_adapters
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))] listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
nics = get_adapters(True) nics = get_adapters(True)
@ -629,11 +620,7 @@ class TcpSrv(object):
pad = self.args.qrp pad = self.args.qrp
zoom = self.args.qrz zoom = self.args.qrz
qrc = qrgen(btxt) qrc = QrCode.encode_binary(btxt)
for zs in self.args.qr_file or []:
self._qr2file(qrc, zs)
if zoom == 0: if zoom == 0:
try: try:
tw, th = termsize() tw, th = termsize()
@ -642,15 +629,13 @@ class TcpSrv(object):
except: except:
zoom = 1 zoom = 1
qr = qr2txt(qrc, zoom, pad) qr = qrc.render(zoom, pad)
if self.args.no_ansi: if self.args.no_ansi:
return "{}\n{}".format(txt, qr) return "{}\n{}".format(txt, qr)
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m" halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
if not fg: if not fg:
halfc = "\033[0;40m{1}\033[0;47m" halfc = "\033[0;40m{1}\033[0;47m"
if nocolor:
halfc = "\033[0;7m{1}\033[0m"
def ansify(m: re.Match) -> str: def ansify(m: re.Match) -> str:
return halfc.format(fg, " " * len(m.group(1)), bg) return halfc.format(fg, " " * len(m.group(1)), bg)
@ -669,29 +654,3 @@ class TcpSrv(object):
t = t.replace("\n", "`\n`") t = t.replace("\n", "`\n`")
return txt + t return txt + t
def _qr2file(self, qrc: QrCode, txt: str):
if ".txt:" in txt or ".svg:" in txt:
ap, zs1, zs2 = txt.rsplit(":", 2)
bg = fg = ""
else:
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
zoom = int(zs1)
pad = int(zs2)
if ap.endswith(".txt"):
if zoom not in (1, 2):
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
with open(ap, "wb") as f:
f.write(qr2txt(qrc, zoom, pad).encode("utf-8"))
elif ap.endswith(".svg"):
with open(ap, "wb") as f:
f.write(qr2svg(qrc, pad).encode("utf-8"))
else:
qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)
def _h2i(self, hs):
try:
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
except:
return None

View file

@ -363,8 +363,7 @@ class Tftpd(object):
yeet("blocked write; folder not world-deletable: /%s" % (vpath,)) yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
xbu = vfs.flags.get("xbu") xbu = vfs.flags.get("xbu")
if xbu: if xbu and not runhook(
hr = runhook(
self.nlog, self.nlog,
None, None,
self.hub.up2k, self.hub.up2k,
@ -380,12 +379,8 @@ class Tftpd(object):
"8.3.8.7", "8.3.8.7",
time.time(), time.time(),
"", "",
) ):
t = hr.get("rejectmsg") or "" yeet("blocked by xbu server config: %r" % (vpath,))
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vpath,)
yeet(t)
if not self.args.tftp_nols and bos.path.isdir(ap): if not self.args.tftp_nols and bos.path.isdir(ap):
return self._ls(vpath, "", 0, True) return self._ls(vpath, "", 0, True)

View file

@ -381,7 +381,7 @@ class ThumbSrv(object):
else: else:
ap_unpk = abspath ap_unpk = abspath
if ap_unpk and not bos.path.exists(tpath): if not bos.path.exists(tpath):
tex = tpath.rsplit(".", 1)[-1] tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3" want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf") want_opus = tex in ("opus", "owa", "caf")
@ -424,14 +424,12 @@ class ThumbSrv(object):
except: except:
pass pass
conv_ok = False
for fun in funs: for fun in funs:
try: try:
if not png_ok and tpath.endswith(".png"): if not png_ok and tpath.endswith(".png"):
raise Exception("png only allowed for waveforms") raise Exception("png only allowed for waveforms")
fun(ap_unpk, ttpath, fmt, vn) fun(ap_unpk, ttpath, fmt, vn)
conv_ok = True
break break
except Exception as ex: except Exception as ex:
msg = "%s could not create thumbnail of %r\n%s" msg = "%s could not create thumbnail of %r\n%s"
@ -453,19 +451,15 @@ class ThumbSrv(object):
except: except:
pass pass
if abspath != ap_unpk and ap_unpk: if abspath != ap_unpk:
wunlink(self.log, ap_unpk, vn.flags) wunlink(self.log, ap_unpk, vn.flags)
try: try:
atomic_move(self.log, ttpath, tpath, vn.flags) atomic_move(self.log, ttpath, tpath, vn.flags)
except Exception as ex: except Exception as ex:
if conv_ok and not os.path.exists(tpath): if not os.path.exists(tpath):
t = "failed to move [%s] to [%s]: %r" t = "failed to move [%s] to [%s]: %r"
self.log(t % (ttpath, tpath, ex), 3) self.log(t % (ttpath, tpath, ex), 3)
elif not conv_ok:
try:
open(tpath, "ab").close()
except:
pass pass
untemp = [] untemp = []
@ -688,7 +682,7 @@ class ThumbSrv(object):
return return
c: Union[str, int] = "90" c: Union[str, int] = "90"
t = "FFmpeg failed (probably a corrupt file):\n" t = "FFmpeg failed (probably a corrupt video file):\n"
if ( if (
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60) (not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
and cmd[-1].lower().endswith(b".webp") and cmd[-1].lower().endswith(b".webp")

View file

@ -53,11 +53,6 @@ class U2idx(object):
self.log("your python does not have sqlite3; searching will be disabled") self.log("your python does not have sqlite3; searching will be disabled")
return return
if self.args.srch_icase:
self._open_db = self._open_db_icase
else:
self._open_db = self._open_db_std
assert sqlite3 # type: ignore # !rm assert sqlite3 # type: ignore # !rm
self.active_id = "" self.active_id = ""
@ -74,16 +69,6 @@ class U2idx(object):
def log(self, msg: str, c: Union[int, str] = 0) -> None: def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("u2idx", msg, c) self.log_func("u2idx", msg, c)
def _open_db_std(self, *args, **kwargs):
assert sqlite3 # type: ignore # !rm
kwargs["check_same_thread"] = False
return sqlite3.connect(*args, **kwargs)
def _open_db_icase(self, *args, **kwargs):
db = self._open_db_std(*args, **kwargs)
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
return db
def shutdown(self) -> None: def shutdown(self) -> None:
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
return return
@ -163,7 +148,8 @@ class U2idx(object):
uri = "" uri = ""
try: try:
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri()) uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
cur = self._open_db(uri, timeout=2, uri=True).cursor() db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
cur = db.cursor()
cur.execute('pragma table_info("up")').fetchone() cur.execute('pragma table_info("up")').fetchone()
self.log("ro: %r" % (db_path,)) self.log("ro: %r" % (db_path,))
except: except:
@ -174,7 +160,7 @@ class U2idx(object):
if not cur: if not cur:
# on windows, this steals the write-lock from up2k.deferred_init -- # 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 # seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
cur = self._open_db(db_path, timeout=2).cursor() cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
self.log("opened %r" % (db_path,)) self.log("opened %r" % (db_path,))
self.cur[ptop] = cur self.cur[ptop] = cur
@ -187,8 +173,6 @@ class U2idx(object):
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
return [], [], False return [], [], False
icase = self.args.srch_icase
q = "" q = ""
v: Union[str, int] = "" v: Union[str, int] = ""
va: list[Union[str, int]] = [] va: list[Union[str, int]] = []
@ -196,7 +180,6 @@ class U2idx(object):
is_key = True is_key = True
is_size = False is_size = False
is_date = False is_date = False
is_wark = False
field_end = "" # closing parenthesis or whatever field_end = "" # closing parenthesis or whatever
kw_key = ["(", ")", "and ", "or ", "not "] kw_key = ["(", ")", "and ", "or ", "not "]
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "] kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
@ -215,8 +198,6 @@ class U2idx(object):
is_key = kw in kw_key is_key = kw in kw_key
uq = uq[len(kw) :] uq = uq[len(kw) :]
ok = True ok = True
if is_wark:
kw = "= "
q += kw q += kw
break break
@ -251,17 +232,9 @@ class U2idx(object):
elif v == "path": elif v == "path":
v = "trim(?||up.rd,'/')" v = "trim(?||up.rd,'/')"
va.append("\nrd") va.append("\nrd")
if icase:
v = "casefold(%s)" % (v,)
elif v == "name": elif v == "name":
v = "up.fn" v = "up.fn"
if icase:
v = "casefold(%s)" % (v,)
elif v == "w":
v = "substr(up.w,1,16)"
is_wark = True
elif v == "tags" or ptn_mt.match(v): elif v == "tags" or ptn_mt.match(v):
have_mt = True have_mt = True
@ -274,7 +247,7 @@ class U2idx(object):
v = "exists(select 1 from mt where mt.w = mtw and " + vq v = "exists(select 1 from mt where mt.w = mtw and " + vq
else: else:
raise Pebkac(400, "invalid key %r" % (v,)) raise Pebkac(400, "invalid key [{}]".format(v))
q += v + " " q += v + " "
continue continue
@ -303,14 +276,6 @@ class U2idx(object):
is_size = False is_size = False
v = int(float(v) * 1024 * 1024) v = int(float(v) * 1024 * 1024)
elif is_wark:
is_wark = False
v = v.strip("*")
if len(v) > 16:
v = v[:16]
if len(v) < 16:
raise Pebkac(400, "w/filehash must be 16+ chars")
else: else:
if v.startswith("*"): if v.startswith("*"):
head = "'%'||" head = "'%'||"
@ -320,12 +285,6 @@ class U2idx(object):
tail = "||'%'" tail = "||'%'"
v = v[:-1] v = v[:-1]
if icase and "casefold(" in q:
try:
v = unicode(v).casefold()
except:
v = unicode(v).lower()
q += " {}?{} ".format(head, tail) q += " {}?{} ".format(head, tail)
va.append(v) va.append(v)
is_key = True is_key = True
@ -360,7 +319,7 @@ class U2idx(object):
uname: str, uname: str,
vols: list[VFS], vols: list[VFS],
uq: str, uq: str,
uv: Union[list[str], list[Union[str, int]]], uv: list[Union[str, int]],
have_mt: bool, have_mt: bool,
sort: bool, sort: bool,
lim: int, lim: int,

View file

@ -18,15 +18,13 @@ from copy import deepcopy
from queue import Queue from queue import Queue
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
from .bos import bos from .bos import bos
from .cfg import vf_bmap, vf_cmap, vf_vmap from .cfg import vf_bmap, vf_cmap, vf_vmap
from .fsutil import Fstab from .fsutil import Fstab
from .mtag import MParser, MTag from .mtag import MParser, MTag
from .util import ( from .util import (
E_FS_CRIT,
E_FS_MEH,
HAVE_SQLITE3, HAVE_SQLITE3,
SYMTIME, SYMTIME,
VF_CAREFUL, VF_CAREFUL,
@ -62,7 +60,6 @@ from .util import (
sfsenc, sfsenc,
spack, spack,
statdir, statdir,
trystat_shutil_copy2,
ub64enc, ub64enc,
unhumanize, unhumanize,
vjoin, vjoin,
@ -94,9 +91,6 @@ ICV_EXTS = set(zsg.split(","))
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv" zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
VCV_EXTS = set(zsg.split(",")) VCV_EXTS = set(zsg.split(","))
zsg = "aif,aiff,alac,ape,flac,m4a,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub"
ACV_EXTS = set(zsg.split(","))
zsg = "nohash noidx xdev xvol" zsg = "nohash noidx xdev xvol"
VF_AFFECTS_INDEXING = set(zsg.split(" ")) VF_AFFECTS_INDEXING = set(zsg.split(" "))
@ -215,7 +209,7 @@ class Up2k(object):
t = "could not initialize sqlite3, will use in-memory registry only" t = "could not initialize sqlite3, will use in-memory registry only"
self.log(t, 3) self.log(t, 3)
self.fstab = Fstab(self.log_func, self.args, True) self.fstab = Fstab(self.log_func, self.args)
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
if self.args.hash_mt < 2: if self.args.hash_mt < 2:
@ -417,11 +411,10 @@ class Up2k(object):
ret: list[tuple[int, str, int, int, int]] = [] ret: list[tuple[int, str, int, int, int]] = []
userset = set([(uname or "\n"), "*"]) userset = set([(uname or "\n"), "*"])
e_d = {}
n = 1000 n = 1000
try: try:
for ptop, tab2 in self.registry.items(): for ptop, tab2 in self.registry.items():
cfg = self.flags.get(ptop, e_d).get("u2abort", 1) cfg = self.flags.get(ptop, {}).get("u2abort", 1)
if not cfg: if not cfg:
continue continue
addr = (ip or "\n") if cfg in (1, 2) else "" addr = (ip or "\n") if cfg in (1, 2) else ""
@ -930,12 +923,6 @@ class Up2k(object):
with self.mutex, self.reg_mutex: with self.mutex, self.reg_mutex:
# only need to protect register_vpath but all in one go feels right # only need to protect register_vpath but all in one go feels right
for vol in vols: for vol in vols:
if bos.path.isfile(vol.realpath):
self.volstate[vol.vpath] = "online (just-a-file)"
t = "NOTE: volume [/%s] is a file, not a folder"
self.log(t % (vol.vpath,))
continue
try: try:
# mkdir gonna happen at snap anyways; # mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vf=vol.flags) bos.makedirs(vol.realpath, vf=vol.flags)
@ -1142,7 +1129,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}" ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}" ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}" fv = "\033[0;36m{}:\033[90m{}"
zs = "bcasechk du_iwho ext_th_d html_head html_head_d html_head_s put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split()) fx = set(zs.split())
fd = vf_bmap() fd = vf_bmap()
fd.update(vf_cmap()) fd.update(vf_cmap())
@ -1496,12 +1483,10 @@ class Up2k(object):
unreg: list[str] = [] unreg: list[str] = []
files: list[tuple[int, int, str]] = [] files: list[tuple[int, int, str]] = []
fat32 = True fat32 = True
cv = vcv = acv = "" cv = vcv = ""
e_d = {}
th_cvd = self.args.th_coversd th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set th_cvds = self.args.th_coversd_set
scan_pr_s = self.args.scan_pr_s
assert self.pp and self.mem_cur # !rm assert self.pp and self.mem_cur # !rm
self.pp.msg = "a%d %s" % (self.pp.n, cdir) self.pp.msg = "a%d %s" % (self.pp.n, cdir)
@ -1608,11 +1593,9 @@ class Up2k(object):
cv = iname cv = iname
elif not vcv and ext in VCV_EXTS and not iname.startswith("."): elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
vcv = iname vcv = iname
elif not acv and ext in ACV_EXTS and not iname.startswith("."):
acv = iname
if not cv: if not cv:
cv = vcv or acv cv = vcv
if not self.args.no_dirsz: if not self.args.no_dirsz:
tnf += len(files) tnf += len(files)
@ -1699,7 +1682,7 @@ class Up2k(object):
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
self.log(t % (top, rp, dts, lmod, dsz, sz)) self.log(t % (top, rp, dts, lmod, dsz, sz))
self.db_rm(db.c, e_d, rd, fn, 0) self.db_rm(db.c, rd, fn, 0)
tfa += 1 tfa += 1
db.n += 1 db.n += 1
in_db = [] in_db = []
@ -1714,7 +1697,7 @@ class Up2k(object):
if nohash or not sz: if nohash or not sz:
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn) wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
else: else:
if sz > 1024 * 1024 * scan_pr_s: if sz > 1024 * 1024:
self.log("file: %r" % (abspath,)) self.log("file: %r" % (abspath,))
try: try:
@ -1722,7 +1705,7 @@ class Up2k(object):
abspath, "a{}, ".format(self.pp.n) abspath, "a{}, ".format(self.pp.n)
) )
except Exception as ex: except Exception as ex:
self._ex_hash(ex, abspath) self.log("hash: %r @ %r" % (ex, abspath))
continue continue
if not hashes: if not hashes:
@ -1736,7 +1719,7 @@ class Up2k(object):
un = "" un = ""
# skip upload hooks by not providing vflags # skip upload hooks by not providing vflags
self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
db.n += 1 db.n += 1
db.nf += 1 db.nf += 1
tfa += 1 tfa += 1
@ -1796,7 +1779,7 @@ class Up2k(object):
rm_files = [x for x in hits if x not in seen_files] rm_files = [x for x in hits if x not in seen_files]
n_rm = len(rm_files) n_rm = len(rm_files)
for fn in rm_files: for fn in rm_files:
self.db_rm(db.c, e_d, rd, fn, 0) self.db_rm(db.c, rd, fn, 0)
if n_rm: if n_rm:
self.log("forgot {} deleted files".format(n_rm)) self.log("forgot {} deleted files".format(n_rm))
@ -1983,7 +1966,7 @@ class Up2k(object):
try: try:
hashes, _ = self._hashlist_from_file(abspath, pf) hashes, _ = self._hashlist_from_file(abspath, pf)
except Exception as ex: except Exception as ex:
self._ex_hash(ex, abspath) self.log("hash: %r @ %r" % (ex, abspath))
continue continue
if not hashes: if not hashes:
@ -2772,7 +2755,7 @@ class Up2k(object):
cur.close() cur.close()
db.close() db.close()
trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak)) shutil.copy2(fsenc(db_path), fsenc(bak))
return self._orz(db_path) return self._orz(db_path)
def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]: def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]:
@ -3009,7 +2992,7 @@ class Up2k(object):
# check if filesystem supports sparse files; # check if filesystem supports sparse files;
# refuse out-of-order / multithreaded uploading if sprs False # refuse out-of-order / multithreaded uploading if sprs False
sprs = self.fstab.get(pdir)[0] != "ng" sprs = self.fstab.get(pdir) != "ng"
if True: if True:
jcur = self.cur.get(ptop) jcur = self.cur.get(ptop)
@ -3147,7 +3130,7 @@ class Up2k(object):
for cur, dp_dir, dp_fn in lost: for cur, dp_dir, dp_fn in lost:
t = "forgetting desynced db entry: %r" t = "forgetting desynced db entry: %r"
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn))) self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"]) self.db_rm(cur, dp_dir, dp_fn, cj["size"])
if c2 and c2 != cur: if c2 and c2 != cur:
c2.connection.commit() c2.connection.commit()
@ -3300,11 +3283,8 @@ class Up2k(object):
job["at"], job["at"],
"", "",
) )
t = hr.get("rejectmsg") or "" if not hr:
if t or not hr: t = "upload blocked by xbu server config: %r" % (dst,)
if not t:
t = "upload blocked by xbu server config: %r"
t = t % (vp,)
self.log(t, 1) self.log(t, 1)
raise Pebkac(403, t) raise Pebkac(403, t)
if hr.get("reloc"): if hr.get("reloc"):
@ -3444,9 +3424,10 @@ class Up2k(object):
cur.connection.commit() cur.connection.commit()
ap = djoin(job["ptop"], job["prel"], job["name"]) ap = djoin(job["ptop"], job["prel"], job["name"])
mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True) times = (int(time.time()), int(cj["lmod"]))
bos.utime(ap, times, False)
self.log("touched %r from %d to %d" % (ap, job["lmod"], mt)) self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"]))
except Exception as ex: except Exception as ex:
self.log("umod failed, %r" % (ex,), 3) self.log("umod failed, %r" % (ex,), 3)
@ -3480,7 +3461,7 @@ class Up2k(object):
vrel = vjoin(job["prel"], fname) vrel = vjoin(job["prel"], fname)
xlink = bool(vf.get("xlink")) xlink = bool(vf.get("xlink"))
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink) self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
except Exception as ex: except Exception as ex:
self.log("skipping replace-relink: %r" % (ex,)) self.log("skipping replace-relink: %r" % (ex,))
finally: finally:
@ -3598,10 +3579,11 @@ class Up2k(object):
t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)" t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)"
self.log(t, 1) self.log(t, 1)
raise Exception(t % (src, fsrc, dst)) raise Exception(t % (src, fsrc, dst))
trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst)) shutil.copy2(fsenc(csrc), fsenc(dst))
if lmod and (not linked or SYMTIME): if lmod and (not linked or SYMTIME):
bos.utime_c(self.log, dst, int(lmod), False) times = (int(time.time()), int(lmod))
bos.utime(dst, times, False)
def handle_chunks( def handle_chunks(
self, ptop: str, wark: str, chashes: list[str] self, ptop: str, wark: str, chashes: list[str]
@ -3684,7 +3666,6 @@ class Up2k(object):
t = t.format(job["name"], nchunks[0][0], coffsets[0][0], cur_sz) t = t.format(job["name"], nchunks[0][0], coffsets[0][0], cur_sz)
raise Pebkac(400, t) raise Pebkac(400, t)
for chash in chashes:
job["busy"][chash] = 1 job["busy"][chash] = 1
job["poke"] = time.time() job["poke"] = time.time()
@ -3692,7 +3673,7 @@ class Up2k(object):
return chashes, chunksize, coffsets, path, job["lmod"], job["size"], job["sprs"] return chashes, chunksize, coffsets, path, job["lmod"], job["size"], job["sprs"]
def fast_confirm_chunks( def fast_confirm_chunks(
self, ptop: str, wark: str, chashes: list[str], locked: list[str] self, ptop: str, wark: str, chashes: list[str]
) -> tuple[int, str]: ) -> tuple[int, str]:
if not self.mutex.acquire(False): if not self.mutex.acquire(False):
return -1, "" return -1, ""
@ -3700,7 +3681,7 @@ class Up2k(object):
self.mutex.release() self.mutex.release()
return -1, "" return -1, ""
try: try:
return self._confirm_chunks(ptop, wark, chashes, locked, False) return self._confirm_chunks(ptop, wark, chashes, chashes)
finally: finally:
self.reg_mutex.release() self.reg_mutex.release()
self.mutex.release() self.mutex.release()
@ -3709,10 +3690,10 @@ class Up2k(object):
self, ptop: str, wark: str, written: list[str], locked: list[str] self, ptop: str, wark: str, written: list[str], locked: list[str]
) -> tuple[int, str]: ) -> tuple[int, str]:
with self.mutex, self.reg_mutex: with self.mutex, self.reg_mutex:
return self._confirm_chunks(ptop, wark, written, locked, True) return self._confirm_chunks(ptop, wark, written, locked)
def _confirm_chunks( def _confirm_chunks(
self, ptop: str, wark: str, written: list[str], locked: list[str], final: bool self, ptop: str, wark: str, written: list[str], locked: list[str]
) -> tuple[int, str]: ) -> tuple[int, str]:
if True: if True:
self.db_act = self.vol_act[ptop] = time.time() self.db_act = self.vol_act[ptop] = time.time()
@ -3724,16 +3705,14 @@ class Up2k(object):
except Exception as ex: except Exception as ex:
return -2, "confirm_chunk, wark(%r)" % (ex,) # type: ignore return -2, "confirm_chunk, wark(%r)" % (ex,) # type: ignore
for chash in locked if final else written: for chash in locked:
job["busy"].pop(chash, None) job["busy"].pop(chash, None)
try: try:
for chash in written: for chash in written:
job["need"].remove(chash) job["need"].remove(chash)
except Exception as ex: except Exception as ex:
for zs in locked: # dead tcp connections can get here by timeout (OK)
if job["busy"].pop(zs, None):
self.log("panic-unlock wark(%s) chunk(%s)" % (wark, zs), 1)
return -2, "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore return -2, "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore
ret = len(job["need"]) ret = len(job["need"])
@ -3777,8 +3756,10 @@ class Up2k(object):
times = (int(time.time()), int(job["lmod"])) times = (int(time.time()), int(job["lmod"]))
t = "no more chunks, setting times %s (%d) on %r" t = "no more chunks, setting times %s (%d) on %r"
self.log(t % (times, bos.path.getsize(dst), dst)) self.log(t % (times, bos.path.getsize(dst), dst))
bos.utime_c(self.log, dst, times[1], False) try:
# the above logmsg (and associated logic) is retained due to unforget.py bos.utime(dst, times)
except:
self.log("failed to utime (%r, %s)" % (dst, times))
zs = "prel name lmod size ptop vtop wark dwrk host user addr" zs = "prel name lmod size ptop vtop wark dwrk host user addr"
z2 = [job[x] for x in zs.split()] z2 = [job[x] for x in zs.split()]
@ -3897,9 +3878,7 @@ class Up2k(object):
return True return True
def db_rm( def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None:
self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, sz: int
) -> None:
sql = "delete from up where rd = ? and fn = ?" sql = "delete from up where rd = ? and fn = ?"
try: try:
r = db.execute(sql, (rd, fn)) r = db.execute(sql, (rd, fn))
@ -3907,23 +3886,10 @@ class Up2k(object):
assert self.mem_cur # !rm assert self.mem_cur # !rm
r = db.execute(sql, s3enc(self.mem_cur, rd, fn)) r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
if not r.rowcount: if r.rowcount:
return
self.volsize[db] -= sz self.volsize[db] -= sz
self.volnfiles[db] -= 1 self.volnfiles[db] -= 1
if "nodirsz" not in vflags:
try:
q = "update ds set nf=nf-1, sz=sz-? where rd=?"
while True:
db.execute(q, (sz, rd))
if not rd:
break
rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
except:
pass
def db_add( def db_add(
self, self,
db: "sqlite3.Cursor", db: "sqlite3.Cursor",
@ -3943,7 +3909,7 @@ class Up2k(object):
skip_xau: bool = False, skip_xau: bool = False,
) -> None: ) -> None:
"""mutex(main) me""" """mutex(main) me"""
self.db_rm(db, vflags, rd, fn, sz) self.db_rm(db, rd, fn, sz)
if not ip: if not ip:
db_ip = "" db_ip = ""
@ -3984,11 +3950,8 @@ class Up2k(object):
at or time.time(), at or time.time(),
"", "",
) )
t = hr.get("rejectmsg") or "" if not hr:
if t or not hr: t = "upload blocked by xau server config"
if not t:
t = "upload blocked by xau server config: %r"
t = t % (djoin(vtop, rd, fn),)
self.log(t, 1) self.log(t, 1)
wunlink(self.log, dst, vflags) wunlink(self.log, dst, vflags)
self.registry[ptop].pop(wark, None) self.registry[ptop].pop(wark, None)
@ -4158,9 +4121,6 @@ class Up2k(object):
except: except:
raise Pebkac(400, "file not found on disk (already deleted?)") raise Pebkac(400, "file not found on disk (already deleted?)")
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
raise Pebkac(400, "file does not exist case-sensitively")
scandir = not self.args.no_scandir scandir = not self.args.no_scandir
if is_dir: if is_dir:
# note: deletion inside shares would require a rewrite here; # note: deletion inside shares would require a rewrite here;
@ -4232,7 +4192,7 @@ class Up2k(object):
xlink = bool(dbv.flags.get("xlink")) xlink = bool(dbv.flags.get("xlink"))
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file( self._forget_file(
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink ptop, volpath, cur, wark, True, st.st_size, xlink
) )
finally: finally:
if cur: if cur:
@ -4285,9 +4245,6 @@ class Up2k(object):
self.db_act = self.vol_act[svn_dbv.realpath] = time.time() self.db_act = self.vol_act[svn_dbv.realpath] = time.time()
st = bos.stat(sabs) st = bos.stat(sabs)
if "bcasechk" in svn.flags and not svn.casechk(srem, False):
raise Pebkac(400, "file does not exist case-sensitively")
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
with self.mutex: with self.mutex:
try: try:
@ -4446,7 +4403,7 @@ class Up2k(object):
b1, b2 = fsenc(sabs), fsenc(dabs) b1, b2 = fsenc(sabs), fsenc(dabs)
is_link = os.path.islink(b1) # due to _relink is_link = os.path.islink(b1) # due to _relink
try: try:
trystat_shutil_copy2(self.log, b1, b2) shutil.copy2(b1, b2)
except: except:
try: try:
wunlink(self.log, dabs, dvn.flags) wunlink(self.log, dabs, dvn.flags)
@ -4506,9 +4463,6 @@ class Up2k(object):
raise Pebkac(400, "mv: cannot move a mountpoint") raise Pebkac(400, "mv: cannot move a mountpoint")
st = bos.lstat(sabs) st = bos.lstat(sabs)
if "bcasechk" in svn.flags and not svn.casechk(srem, False):
raise Pebkac(400, "file does not exist case-sensitively")
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
with self.mutex: with self.mutex:
try: try:
@ -4696,14 +4650,7 @@ class Up2k(object):
with self.reg_mutex: with self.reg_mutex:
has_dupes = self._forget_file( has_dupes = self._forget_file(
svn.realpath, svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink
srem,
svn.flags,
c1,
w,
is_xvol,
fsize_ or fsize,
xlink,
) )
if not is_xvol: if not is_xvol:
@ -4755,7 +4702,7 @@ class Up2k(object):
b1, b2 = fsenc(sabs), fsenc(dabs) b1, b2 = fsenc(sabs), fsenc(dabs)
is_link = os.path.islink(b1) # due to _relink is_link = os.path.islink(b1) # due to _relink
try: try:
trystat_shutil_copy2(self.log, b1, b2) shutil.copy2(b1, b2)
except: except:
try: try:
wunlink(self.log, dabs, dvn.flags) wunlink(self.log, dabs, dvn.flags)
@ -4848,7 +4795,6 @@ class Up2k(object):
self, self,
ptop: str, ptop: str,
vrem: str, vrem: str,
vflags: dict[str, Any],
cur: Optional["sqlite3.Cursor"], cur: Optional["sqlite3.Cursor"],
wark: Optional[str], wark: Optional[str],
drop_tags: bool, drop_tags: bool,
@ -4873,7 +4819,7 @@ class Up2k(object):
q = "delete from mt where w=?" q = "delete from mt where w=?"
cur.execute(q, (wark[:16],)) cur.execute(q, (wark[:16],))
self.db_rm(cur, vflags, srd, sfn, sz) self.db_rm(cur, srd, sfn, sz)
reg = self.registry.get(ptop) reg = self.registry.get(ptop)
if reg: if reg:
@ -4962,10 +4908,7 @@ class Up2k(object):
mt = bos.path.getmtime(slabs, False) mt = bos.path.getmtime(slabs, False)
flags = self.flags.get(ptop) or {} flags = self.flags.get(ptop) or {}
atomic_move(self.log, sabs, slabs, flags) atomic_move(self.log, sabs, slabs, flags)
try:
bos.utime(slabs, (int(time.time()), int(mt)), False) bos.utime(slabs, (int(time.time()), int(mt)), False)
except:
self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
self._symlink(slabs, sabs, flags, False, is_mv=True) self._symlink(slabs, sabs, flags, False, is_mv=True)
full[slabs] = (ptop, rem) full[slabs] = (ptop, rem)
sabs = slabs sabs = slabs
@ -5038,7 +4981,7 @@ class Up2k(object):
for k in cj["hash"]: for k in cj["hash"]:
if not self.r_hash.match(k): if not self.r_hash.match(k):
raise Pebkac( raise Pebkac(
400, "at least one hash is not according to spec: %r" % (k,) 400, "at least one hash is not according to spec: {}".format(k)
) )
# try to use client-provided timestamp, don't care if it fails somehow # try to use client-provided timestamp, don't care if it fails somehow
@ -5095,16 +5038,6 @@ class Up2k(object):
return ret, st return ret, st
def _ex_hash(self, ex: Exception, ap: str) -> None:
eno = getattr(ex, "errno", 0)
if eno in E_FS_MEH:
return self.log("hashing failed; %r @ %r" % (ex, ap))
if eno not in E_FS_CRIT:
return self.log("hashing failed; %r @ %r\n%s" % (ex, ap, min_ex()), 3)
t = "hashing failed; %r @ %r\n%s\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\n"
t2 = "" if ANYWIN or MACOS else "dmesg and "
return self.log(t % (ex, ap, min_ex(), t2), 1)
def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]: def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]:
pdir = djoin(job["ptop"], job["prel"]) pdir = djoin(job["ptop"], job["prel"])
if not job["size"]: if not job["size"]:
@ -5138,9 +5071,7 @@ class Up2k(object):
job["t0"], job["t0"],
"", "",
) )
t = hr.get("rejectmsg") or "" if not hr:
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vp_chk,) t = "upload blocked by xbu server config: %r" % (vp_chk,)
self.log(t, 1) self.log(t, 1)
raise Pebkac(403, t) raise Pebkac(403, t)
@ -5203,7 +5134,7 @@ class Up2k(object):
sprs = False sprs = False
if not ANYWIN and sprs and sz > 1024 * 1024: if not ANYWIN and sprs and sz > 1024 * 1024:
fs, mnt = self.fstab.get(pdir) fs = self.fstab.get(pdir)
if fs == "ok": if fs == "ok":
pass pass
elif "nosparse" in vf: elif "nosparse" in vf:
@ -5289,21 +5220,17 @@ class Up2k(object):
self.log("\n".join([t] + vis)) self.log("\n".join([t] + vis))
for job in rm: for job in rm:
del reg[job["wark"]] del reg[job["wark"]]
rsv_cleared = False
try: try:
# remove the filename reservation # remove the filename reservation
path = djoin(job["ptop"], job["prel"], job["name"]) path = djoin(job["ptop"], job["prel"], job["name"])
if bos.path.getsize(path) == 0: if bos.path.getsize(path) == 0:
bos.unlink(path) bos.unlink(path)
rsv_cleared = True
except: except:
pass pass
try: try:
if len(job["hash"]) == len(job["need"]) or ( if len(job["hash"]) == len(job["need"]):
rsv_cleared and "rm_partial" in self.flags[job["ptop"]] # PARTIAL is empty, delete that too
):
# PARTIAL is empty (hash==need) or --rm-partial, so delete that too
path = djoin(job["ptop"], job["prel"], job["tnam"]) path = djoin(job["ptop"], job["prel"], job["tnam"])
bos.unlink(path) bos.unlink(path)
except: except:

View file

@ -52,9 +52,9 @@ from .__init__ import (
VT100, VT100,
WINDOWS, WINDOWS,
EnvParams, EnvParams,
unicode,
) )
from .__version__ import S_BUILD_DT, S_VERSION from .__version__ import S_BUILD_DT, S_VERSION
from .stolen import surrogateescape
try: try:
from datetime import datetime, timezone from datetime import datetime, timezone
@ -80,9 +80,6 @@ except:
if PY2: if PY2:
range = xrange # type: ignore range = xrange # type: ignore
from .stolen import surrogateescape
surrogateescape.register_surrogateescape()
if sys.version_info >= (3, 7) or ( if sys.version_info >= (3, 7) or (
@ -113,17 +110,9 @@ E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE") E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
E_ACCESS = _ens("EACCES WSAEACCES") E_ACCESS = _ens("EACCES WSAEACCES")
E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH") E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
E_FS_MEH = _ens("EPERM EACCES ENOENT ENOTCAPABLE")
E_FS_CRIT = _ens("EIO EFAULT EUCLEAN ENOTBLK")
IP6ALL = "0:0:0:0:0:0:0:0" IP6ALL = "0:0:0:0:0:0:0:0"
IP6_LL = ("fe8", "fe9", "fea", "feb")
IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254")
UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._"
BC_CDISP = UC_CDISP.encode("ascii")
UC_CDISP_SET = set(UC_CDISP)
BC_CDISP_SET = set(BC_CDISP)
try: try:
import fcntl import fcntl
@ -138,25 +127,6 @@ try:
except: except:
pass pass
try:
if os.environ.get("PRTY_NO_IFADDR"):
raise Exception()
try:
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_IFADDR"):
raise ImportError()
from .stolen.ifaddr import get_adapters
except ImportError:
from ifaddr import get_adapters
HAVE_IFADDR = True
except:
HAVE_IFADDR = False
def get_adapters(include_unconfigured=False):
return []
try: try:
if os.environ.get("PRTY_NO_SQLITE"): if os.environ.get("PRTY_NO_SQLITE"):
raise Exception() raise Exception()
@ -194,11 +164,6 @@ try:
except: except:
pass pass
if os.getenv("PRTY_MODSPEC"):
from inspect import getsourcefile
print("PRTY_MODSPEC: ifaddr:", getsourcefile(get_adapters))
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
import types import types
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
@ -295,6 +260,7 @@ RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
BOS_SEP = ("%s" % (os.sep,)).encode("ascii") BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
surrogateescape.register_surrogateescape()
if WINDOWS and PY2: if WINDOWS and PY2:
FS_ENCODING = "utf-8" FS_ENCODING = "utf-8"
else: else:
@ -403,13 +369,6 @@ DAV_ALLPROP_L = [
DAV_ALLPROPS = set(DAV_ALLPROP_L) DAV_ALLPROPS = set(DAV_ALLPROP_L)
FAVICON_MIMES = {
"gif": "image/gif",
"png": "image/png",
"svg": "image/svg+xml",
}
MIMES = { MIMES = {
"opus": "audio/ogg; codecs=opus", "opus": "audio/ogg; codecs=opus",
"owa": "audio/webm; codecs=opus", "owa": "audio/webm; codecs=opus",
@ -464,7 +423,7 @@ EXTS["vnd.mozilla.apng"] = "png"
MAGIC_MAP = {"jpeg": "jpg"} MAGIC_MAP = {"jpeg": "jpg"}
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime" DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime"
DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash" DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"
@ -580,8 +539,6 @@ def py_desc() -> str:
ofs = py_ver.find(".final.") ofs = py_ver.find(".final.")
if ofs > 0: if ofs > 0:
py_ver = py_ver[:ofs] py_ver = py_ver[:ofs]
if "free-threading" in sys.version:
py_ver += "t"
host_os = platform.system() host_os = platform.system()
compiler = platform.python_compiler().split("http")[0] compiler = platform.python_compiler().split("http")[0]
@ -692,9 +649,6 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
with open(ap, "rb") as f: with open(ap, "rb") as f:
buf = f.read() buf = f.read()
if buf.startswith(b"\xef\xbb\xbf"):
buf = buf[3:]
try: try:
return buf.decode("utf-8", "strict") return buf.decode("utf-8", "strict")
except UnicodeDecodeError as ex: except UnicodeDecodeError as ex:
@ -1160,18 +1114,16 @@ class ProgressPrinter(threading.Thread):
sigblock() sigblock()
tp = 0 tp = 0
msg = None msg = None
slp_pr = self.args.scan_pr_r no_stdout = self.args.q
slp_ps = min(slp_pr, self.args.scan_st_r)
no_stdout = self.args.q or slp_pr == slp_ps
fmt = " {}\033[K\r" if VT100 else " {} $\r" fmt = " {}\033[K\r" if VT100 else " {} $\r"
while not self.end: while not self.end:
time.sleep(slp_ps) time.sleep(0.1)
if msg == self.msg or self.end: if msg == self.msg or self.end:
continue continue
msg = self.msg msg = self.msg
now = time.time() now = time.time()
if msg and now - tp >= slp_pr: if msg and now - tp > 10:
tp = now tp = now
self.log("progress: %r" % (msg,), 6) self.log("progress: %r" % (msg,), 6)
@ -1229,21 +1181,21 @@ class MTHash(object):
for nch in range(nchunks): for nch in range(nchunks):
self.work_q.put(nch) self.work_q.put(nch)
ex: Optional[Exception] = None ex = ""
for nch in range(nchunks): for nch in range(nchunks):
qe = self.done_q.get() qe = self.done_q.get()
try: try:
nch, dig, ofs, csz = qe nch, dig, ofs, csz = qe
chunks[nch] = (dig, ofs, csz) chunks[nch] = (dig, ofs, csz)
except: except:
ex = ex or qe # type: ignore ex = ex or str(qe)
if pp: if pp:
mb = (fsz - nch * chunksz) // (1024 * 1024) mb = (fsz - nch * chunksz) // (1024 * 1024)
pp.msg = prefix + str(mb) + suffix pp.msg = prefix + str(mb) + suffix
if ex: if ex:
raise ex raise Exception(ex)
ret = [] ret = []
for n in range(nchunks): for n in range(nchunks):
@ -1260,7 +1212,7 @@ class MTHash(object):
try: try:
v = self.hash_at(ofs) v = self.hash_at(ofs)
except Exception as ex: except Exception as ex:
v = ex # type: ignore v = str(ex) # type: ignore
self.done_q.put(v) self.done_q.put(v)
@ -1618,11 +1570,9 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
bvp = vol.vpath.encode("utf-8") bvp = vol.vpath.encode("utf-8")
bvph = b"$hist(/" + bvp + b")" bvph = b"$hist(/" + bvp + b")"
if bap:
txt = txt.replace(bap, bvp) txt = txt.replace(bap, bvp)
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
if bhp:
txt = txt.replace(bhp, bvph) txt = txt.replace(bhp, bvph)
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph) txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
if vol.histpath != vol.dbpath: if vol.histpath != vol.dbpath:
@ -1802,12 +1752,12 @@ class MultipartParser(object):
continue continue
if m.group(1).lower() != "form-data": if m.group(1).lower() != "form-data":
raise Pebkac(400, "not form-data: %r" % (ln,)) raise Pebkac(400, "not form-data: {}".format(ln))
try: try:
field = self.re_cdisp_field.match(ln).group(1) # type: ignore field = self.re_cdisp_field.match(ln).group(1) # type: ignore
except: except:
raise Pebkac(400, "missing field name: %r" % (ln,)) raise Pebkac(400, "missing field name: {}".format(ln))
try: try:
fn = self.re_cdisp_file.match(ln).group(1) # type: ignore fn = self.re_cdisp_file.match(ln).group(1) # type: ignore
@ -1979,7 +1929,7 @@ def get_boundary(headers: dict[str, str]) -> str:
ct = headers["content-type"] ct = headers["content-type"]
m = re.match(ptn, ct, re.IGNORECASE) m = re.match(ptn, ct, re.IGNORECASE)
if not m: if not m:
raise Pebkac(400, "invalid content-type for a multipart post: %r" % (ct,)) raise Pebkac(400, "invalid content-type for a multipart post: {}".format(ct))
return m.group(2) return m.group(2)
@ -2121,29 +2071,6 @@ def gencookie(
) )
def gen_content_disposition(fn: str) -> str:
safe = UC_CDISP_SET
bsafe = BC_CDISP_SET
fn = fn.replace("/", "_").replace("\\", "_")
zb = fn.encode("utf-8", "xmlcharrefreplace")
if not PY2:
zbl = [
chr(x).encode("utf-8")
if x in bsafe
else "%{:02X}".format(x).encode("ascii")
for x in zb
]
else:
zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb]
ufn = b"".join(zbl).decode("ascii")
afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".")
while ".." in afn:
afn = afn.replace("..", ".")
return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn)
def humansize(sz: float, terse: bool = False) -> str: def humansize(sz: float, terse: bool = False) -> str:
for unit in HUMANSIZE_UNITS: for unit in HUMANSIZE_UNITS:
if sz < 1024: if sz < 1024:
@ -2151,7 +2078,6 @@ def humansize(sz: float, terse: bool = False) -> str:
sz /= 1024.0 sz /= 1024.0
assert unit # type: ignore # !rm
if terse: if terse:
return "%s%s" % (str(sz)[:4].rstrip("."), unit[:1]) return "%s%s" % (str(sz)[:4].rstrip("."), unit[:1])
else: else:
@ -2304,14 +2230,14 @@ def odfusion(
ret = base.copy() ret = base.copy()
if oth.startswith("+"): if oth.startswith("+"):
for k in words1: for k in words1:
ret[k] = True # type: ignore ret[k] = True
elif oth[:1] in ("-", "/"): elif oth[:1] in ("-", "/"):
for k in words1: for k in words1:
ret.pop(k, None) # type: ignore ret.pop(k, None)
else: else:
ret = ODict.fromkeys(words0, True) ret = ODict.fromkeys(words0, True)
return ret # type: ignore return ret
def ipnorm(ip: str) -> str: def ipnorm(ip: str) -> str:
@ -2683,24 +2609,6 @@ def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) ->
os.fchown(fno, vf["uid"], vf["gid"]) os.fchown(fno, vf["uid"], vf["gid"])
def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
try:
return shutil.copy2(src, dst)
except:
# ignore failed mtime on linux+ntfs; for example:
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
_, _, tb = sys.exc_info()
for _, _, fun, _ in traceback.extract_tb(tb):
if fun == "copystat":
if log:
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\n%s"
log(t % (src, dst, min_ex()), 3)
return dst # close enough
raise
def _fs_mvrm( def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool: ) -> bool:
@ -2739,7 +2647,7 @@ def _fs_mvrm(
t = "something appeared at dst; aborting rename %r ==> %r" t = "something appeared at dst; aborting rename %r ==> %r"
log(t % (src, dst), 1) log(t % (src, dst), 1)
return False return False
osfun(*args) # type: ignore osfun(*args)
if attempt: if attempt:
now = time.time() now = time.time()
t = "%sd in %.2f sec, attempt %d: %r" t = "%sd in %.2f sec, attempt %d: %r"
@ -2789,7 +2697,7 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
os.unlink(bdst) os.unlink(bdst)
except: except:
pass pass
shutil.move(bsrc, bdst) # type: ignore shutil.move(bsrc, bdst)
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool: def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
@ -2832,8 +2740,6 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
if not ANYWIN and not MACOS: if not ANYWIN and not MACOS:
def siocoutq(sck: socket.socket) -> int: def siocoutq(sck: socket.socket) -> int:
assert fcntl # type: ignore # !rm
assert termios # type: ignore # !rm
# SIOCOUTQ^sockios.h == TIOCOUTQ^ioctl.h # SIOCOUTQ^sockios.h == TIOCOUTQ^ioctl.h
try: try:
zb = fcntl.ioctl(sck.fileno(), termios.TIOCOUTQ, b"AAAA") zb = fcntl.ioctl(sck.fileno(), termios.TIOCOUTQ, b"AAAA")
@ -2953,6 +2859,8 @@ def read_socket_chunked(
def list_ips() -> list[str]: def list_ips() -> list[str]:
from .stolen.ifaddr import get_adapters
ret: set[str] = set() ret: set[str] = set()
for nic in get_adapters(): for nic in get_adapters():
for ipo in nic.ips: for ipo in nic.ips:
@ -3195,7 +3103,7 @@ def sendfile_kern(
try: try:
req = min(0x2000000, upper - ofs) # 32 MiB req = min(0x2000000, upper - ofs) # 32 MiB
if use_poll: if use_poll:
poll.poll(10000) # type: ignore poll.poll(10000)
else: else:
select.select([], [out_fd], [], 10) select.select([], [out_fd], [], 10)
n = os.sendfile(out_fd, in_fd, ofs, req) n = os.sendfile(out_fd, in_fd, ofs, req)
@ -3251,9 +3159,8 @@ def statdir(
else: else:
src = "listdir" src = "listdir"
fun: Any = os.lstat if lstat else os.stat fun: Any = os.lstat if lstat else os.stat
btop_ = os.path.join(btop, b"")
for name in os.listdir(btop): for name in os.listdir(btop):
abspath = btop_ + name abspath = os.path.join(btop, name)
try: try:
yield (fsdec(name), fun(abspath)) yield (fsdec(name), fun(abspath))
except Exception as ex: except Exception as ex:
@ -3292,9 +3199,7 @@ def rmdirs(
stats = statdir(logger, scandir, lstat, top, False) stats = statdir(logger, scandir, lstat, top, False)
dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)] dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]
if dirs: dirs = [os.path.join(top, x) for x in dirs]
top_ = os.path.join(top, "")
dirs = [top_ + x for x in dirs]
ok = [] ok = []
ng = [] ng = []
for d in reversed(dirs): for d in reversed(dirs):
@ -3485,9 +3390,7 @@ NICEB = NICES.encode("utf-8")
def runcmd( def runcmd(
argv: Union[list[bytes], list[str], list["LiteralString"]], argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
timeout: Optional[float] = None,
**ka: Any
) -> tuple[int, str, str]: ) -> tuple[int, str, str]:
isbytes = isinstance(argv[0], (bytes, bytearray)) isbytes = isinstance(argv[0], (bytes, bytearray))
oom = ka.pop("oom", 0) # 0..1000 oom = ka.pop("oom", 0) # 0..1000
@ -3506,19 +3409,19 @@ def runcmd(
if ANYWIN: if ANYWIN:
if isbytes: if isbytes:
if argv[0] in CMD_EXEB: if argv[0] in CMD_EXEB:
argv[0] += b".exe" # type: ignore argv[0] += b".exe"
else: else:
if argv[0] in CMD_EXES: if argv[0] in CMD_EXES:
argv[0] += ".exe" # type: ignore argv[0] += ".exe"
if ka.pop("nice", None): if ka.pop("nice", None):
if WINDOWS: if WINDOWS:
ka["creationflags"] = 0x4000 ka["creationflags"] = 0x4000
elif NICEB: elif NICEB:
if isbytes: if isbytes:
argv = [NICEB] + argv # type: ignore argv = [NICEB] + argv
else: else:
argv = [NICES] + argv # type: ignore argv = [NICES] + argv
p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka) p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka)
@ -3530,10 +3433,10 @@ def runcmd(
pass pass
if not timeout or PY2: if not timeout or PY2:
bout, berr = p.communicate(sin) # type: ignore bout, berr = p.communicate(sin)
else: else:
try: try:
bout, berr = p.communicate(sin, timeout=timeout) # type: ignore bout, berr = p.communicate(sin, timeout=timeout)
except sp.TimeoutExpired: except sp.TimeoutExpired:
if kill == "n": if kill == "n":
return -18, "", "" # SIGCONT; leave it be return -18, "", "" # SIGCONT; leave it be
@ -3543,7 +3446,7 @@ def runcmd(
killtree(p.pid) killtree(p.pid)
try: try:
bout, berr = p.communicate(timeout=1) # type: ignore bout, berr = p.communicate(timeout=1)
except: except:
bout = b"" bout = b""
berr = b"" berr = b""
@ -3629,13 +3532,11 @@ def retchk(
def _parsehook( def _parsehook(
log: Optional["NamedLogger"], cmd: str log: Optional["NamedLogger"], cmd: str
) -> tuple[str, bool, bool, bool, bool, bool, float, dict[str, Any], list[str]]: ) -> tuple[str, bool, bool, bool, float, dict[str, Any], list[str]]:
areq = "" areq = ""
chk = False chk = False
fork = False fork = False
jtxt = False jtxt = False
imp = False
sin = False
wait = 0.0 wait = 0.0
tout = 0.0 tout = 0.0
kill = "t" kill = "t"
@ -3649,10 +3550,6 @@ def _parsehook(
fork = True fork = True
elif arg == "j": elif arg == "j":
jtxt = True jtxt = True
elif arg == "I":
imp = True
elif arg == "s":
sin = True
elif arg.startswith("w"): elif arg.startswith("w"):
wait = float(arg[1:]) wait = float(arg[1:])
elif arg.startswith("t"): elif arg.startswith("t"):
@ -3697,7 +3594,7 @@ def _parsehook(
argv[0] = os.path.expandvars(os.path.expanduser(argv[0])) argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))
return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv return areq, chk, fork, jtxt, wait, sp_ka, argv
def runihook( def runihook(
@ -3707,7 +3604,7 @@ def runihook(
vol: "VFS", vol: "VFS",
ups: list[tuple[str, int, int, str, str, str, int, str]], ups: list[tuple[str, int, int, str, str, str, int, str]],
) -> bool: ) -> bool:
_, chk, _, fork, _, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
bcmd = [sfsenc(x) for x in acmd] bcmd = [sfsenc(x) for x in acmd]
if acmd[0].endswith(".py"): if acmd[0].endswith(".py"):
bcmd = [sfsenc(pybin)] + bcmd bcmd = [sfsenc(pybin)] + bcmd
@ -3886,7 +3783,7 @@ def _runhook(
txt: str, txt: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
ret = {"rc": 0} ret = {"rc": 0}
areq, chk, imp, fork, sin, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) areq, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
if areq: if areq:
for ch in areq: for ch in areq:
if ch not in perms: if ch not in perms:
@ -3894,7 +3791,7 @@ def _runhook(
if log: if log:
log(t % (uname, cmd, areq, perms)) log(t % (uname, cmd, areq, perms))
return ret # fallthrough to next hook return ret # fallthrough to next hook
if imp or jtxt: if jtxt:
ja = { ja = {
"ap": ap, "ap": ap,
"vp": vp, "vp": vp,
@ -3908,10 +3805,6 @@ def _runhook(
"src": src, "src": src,
"txt": txt, "txt": txt,
} }
if imp:
ja["log"] = log
mod = loadpy(acmd[0], False)
return mod.main(ja)
arg = json.dumps(ja) arg = json.dumps(ja)
else: else:
arg = txt or ap arg = txt or ap
@ -3922,11 +3815,7 @@ def _runhook(
raise Exception("zmq says %d" % (zi,)) raise Exception("zmq says %d" % (zi,))
return {"rc": 0, "stdout": zs} return {"rc": 0, "stdout": zs}
if sin:
sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace")
else:
acmd += [arg] acmd += [arg]
if acmd[0].endswith(".py"): if acmd[0].endswith(".py"):
acmd = [pybin] + acmd acmd = [pybin] + acmd
@ -3980,7 +3869,7 @@ def runhook(
txt: str, txt: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
assert broker or up2k # !rm assert broker or up2k # !rm
args = (broker or up2k).args # type: ignore args = (broker or up2k).args
verbose = args.hook_v verbose = args.hook_v
vp = vp.replace("\\", "/") vp = vp.replace("\\", "/")
ret = {"rc": 0} ret = {"rc": 0}
@ -3998,7 +3887,6 @@ def runhook(
if broker: if broker:
broker.say("up2k.hook_fx", k, v, vp) broker.say("up2k.hook_fx", k, v, vp)
else: else:
assert up2k # !rm
up2k.fx_backlog.append((k, v, vp)) up2k.fx_backlog.append((k, v, vp))
elif k == "reloc" and v: elif k == "reloc" and v:
# idk, just take the last one ig # idk, just take the last one ig
@ -4011,7 +3899,7 @@ def runhook(
else: else:
ret[k] = v ret[k] = v
except Exception as ex: except Exception as ex:
(log or print)("hook: %r, %s" % (ex, ex)) (log or print)("hook: {}".format(ex))
if ",c," in "," + cmd: if ",c," in "," + cmd:
return {} return {}
break break
@ -4158,8 +4046,6 @@ def termsize() -> tuple[int, int]:
env = os.environ env = os.environ
def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]: def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]:
assert fcntl # type: ignore # !rm
assert termios # type: ignore # !rm
try: try:
cr = sunpack(b"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"AAAA")) cr = sunpack(b"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"AAAA"))
return cr[::-1] return cr[::-1]
@ -4289,7 +4175,7 @@ def _pkg_resource_exists(pkg: str, name: str) -> bool:
def stat_resource(E: EnvParams, name: str): def stat_resource(E: EnvParams, name: str):
path = E.mod_ + name path = os.path.join(E.mod, name)
if os.path.exists(path): if os.path.exists(path):
return os.stat(fsenc(path)) return os.stat(fsenc(path))
return None return None
@ -4336,7 +4222,7 @@ def _has_resource(name: str):
def has_resource(E: EnvParams, name: str): def has_resource(E: EnvParams, name: str):
return _has_resource(name) or os.path.exists(E.mod_ + name) return _has_resource(name) or os.path.exists(os.path.join(E.mod, name))
def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]: def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
@ -4361,7 +4247,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream) stream = codecs.getreader(enc)(stream)
return stream return stream
ap = E.mod_ + name ap = os.path.join(E.mod, name)
if PY2: if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore return codecs.open(ap, "r", encoding=enc) # type: ignore

View file

@ -22,9 +22,8 @@ window.baguetteBox = (function () {
afterHide: null, afterHide: null,
duringHide: null, duringHide: null,
onChange: null, onChange: null,
readDirRtl: false,
}, },
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnReadDir, btnClose, overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnClose,
currentGallery = [], currentGallery = [],
currentIndex = 0, currentIndex = 0,
isOverlayVisible = false, isOverlayVisible = false,
@ -45,7 +44,6 @@ window.baguetteBox = (function () {
loopA = null, loopA = null,
loopB = null, loopB = null,
url_ts = null, url_ts = null,
un_pp = 0,
resume_mp = false; resume_mp = false;
var onFSC = function (e) { var onFSC = function (e) {
@ -74,10 +72,10 @@ window.baguetteBox = (function () {
var touchEvent = e.touches[0] || e.changedTouches[0]; var touchEvent = e.touches[0] || e.changedTouches[0];
if (touchEvent.pageX - touch.startX > 40) { if (touchEvent.pageX - touch.startX > 40) {
touchFlag = true; touchFlag = true;
showLeftImage(); showPreviousImage();
} else if (touchEvent.pageX - touch.startX < -40) { } else if (touchEvent.pageX - touch.startX < -40) {
touchFlag = true; touchFlag = true;
showRightImage(); showNextImage();
} else if (touch.startY - touchEvent.pageY > 100) { } else if (touch.startY - touchEvent.pageY > 100) {
hideOverlay(); hideOverlay();
} }
@ -115,9 +113,9 @@ window.baguetteBox = (function () {
scrollTimer = Date.now(); scrollTimer = Date.now();
if (d > 0) if (d > 0)
showNextImageIgnoreReadDir(); showNextImage();
else else
showPreviousImageIgnoreReadDir(); showPreviousImage();
}; };
var trapFocusInsideOverlay = function (e) { var trapFocusInsideOverlay = function (e) {
@ -213,7 +211,6 @@ window.baguetteBox = (function () {
'<div id="bbox-btns">' + '<div id="bbox-btns">' +
'<button id="bbox-help" type="button">?</button>' + '<button id="bbox-help" type="button">?</button>' +
'<button id="bbox-anim" type="button" tt="a">-</button>' + '<button id="bbox-anim" type="button" tt="a">-</button>' +
'<button id="bbox-readdir" type="button" tt="a">ltr</button>' +
'<button id="bbox-rotl" type="button">↶</button>' + '<button id="bbox-rotl" type="button">↶</button>' +
'<button id="bbox-rotr" type="button">↷</button>' + '<button id="bbox-rotr" type="button">↷</button>' +
'<button id="bbox-tsel" type="button">sel</button>' + '<button id="bbox-tsel" type="button">sel</button>' +
@ -231,7 +228,6 @@ window.baguetteBox = (function () {
btnNext = ebi('bbox-next'); btnNext = ebi('bbox-next');
btnHelp = ebi('bbox-help'); btnHelp = ebi('bbox-help');
btnAnim = ebi('bbox-anim'); btnAnim = ebi('bbox-anim');
btnReadDir = ebi('bbox-readdir');
btnRotL = ebi('bbox-rotl'); btnRotL = ebi('bbox-rotl');
btnRotR = ebi('bbox-rotr'); btnRotR = ebi('bbox-rotr');
btnSel = ebi('bbox-tsel'); btnSel = ebi('bbox-tsel');
@ -256,7 +252,7 @@ window.baguetteBox = (function () {
['S', 'toggle file selection'], ['S', 'toggle file selection'],
['space, P, K', 'video: play / pause'], ['space, P, K', 'video: play / pause'],
['U', 'video: seek 10sec back'], ['U', 'video: seek 10sec back'],
['O', 'video: seek 10sec ahead'], ['P', 'video: seek 10sec ahead'],
['0..9', 'video: seek 0%..90%'], ['0..9', 'video: seek 0%..90%'],
['M', 'video: toggle mute'], ['M', 'video: toggle mute'],
['V', 'video: toggle loop'], ['V', 'video: toggle loop'],
@ -282,68 +278,58 @@ window.baguetteBox = (function () {
if (modal.busy) if (modal.busy)
return; return;
if (e.key == '?')
return halp();
if (anymod(e, true)) if (anymod(e, true))
return; return;
var k = (e.key || e.code) + '', v = vid(); var k = (e.code || e.key) + '', v = vid(), pos = -1;
if (k.startsWith('Key')) if (k == "BracketLeft")
k = k.slice(3);
else if (k.startsWith('Digit'))
k = k.slice(5);
var kl = k.toLowerCase();
if (k == '?')
return halp();
if (k == "[" || k == "BracketLeft")
setloop(1); setloop(1);
else if (k == "]" || k == "BracketRight") else if (k == "BracketRight")
setloop(2); setloop(2);
else if (e.shiftKey && kl != "r") else if (e.shiftKey && k != "KeyR" && k != "R")
return; return;
else if (k == "ArrowLeft" || k == "Left" || kl == "j") else if (k == "ArrowLeft" || k == "KeyJ" || k == "Left" || k == "j")
showLeftImage(); showPreviousImage();
else if (k == "ArrowRight" || k == "Right" || kl == "l") else if (k == "ArrowRight" || k == "KeyL" || k == "Right" || k == "l")
showRightImage(); showNextImage();
else if (k == "Escape" || k == "Esc") else if (k == "Escape" || k == "Esc")
hideOverlay(); hideOverlay();
else if (k == "Home") else if (k == "Home")
showFirstImage(e); showFirstImage(e);
else if (k == "End") else if (k == "End")
showLastImage(e); showLastImage(e);
else if (k == "Space" || k == "Spacebar" || kl == " " || kl == "p" || kl == "k") else if (k == "Space" || k == "KeyP" || k == "KeyK")
playpause(); playpause();
else if (kl == "u" || kl == "o") else if (k == "KeyU" || k == "KeyO")
relseek(kl == "u" ? -10 : 10); relseek(k == "KeyU" ? -10 : 10);
else if (v && /^[0-9]$/.test(k)) else if (k.indexOf('Digit') === 0 && v)
v.currentTime = v.duration * parseInt(k) * 0.1; v.currentTime = v.duration * parseInt(k.slice(-1)) * 0.1;
else if (kl == "m" && v) { else if (k == "KeyM" && v) {
v.muted = vmute = !vmute; v.muted = vmute = !vmute;
mp_ctl(); mp_ctl();
} }
else if (kl == "v" && v) { else if (k == "KeyV" && v) {
vloop = !vloop; vloop = !vloop;
vnext = vnext && !vloop; vnext = vnext && !vloop;
setVmode(); setVmode();
} }
else if (kl == "c" && v) { else if (k == "KeyC" && v) {
vnext = !vnext; vnext = !vnext;
vloop = vloop && !vnext; vloop = vloop && !vnext;
setVmode(); setVmode();
} }
else if (kl == "f") else if (k == "KeyF")
tglfull(); tglfull();
else if (kl == "s") else if (k == "KeyS" || k == "s")
tglsel(); tglsel();
else if (kl == "r") else if (k == "KeyR" || k == "r" || k == "R")
rotn(e.shiftKey ? -1 : 1); rotn(e.shiftKey ? -1 : 1);
else if (kl == "y") else if (k == "KeyY")
dlpic(); dlpic();
else
return;
return ev(e);
} }
function anim() { function anim() {
@ -356,21 +342,6 @@ window.baguetteBox = (function () {
tt.show.call(this); tt.show.call(this);
} }
function toggleReadDir() {
var o = options,
next = options.readDirRtl ? "ltr" : "rtl";
swrite('greaddir', next);
slider.className = "no-transition";
options = {};
setOptions(o);
updateOffset(true);
window.getComputedStyle(slider).opacity; // force a restyle
slider.className = "";
if (tt.en)
tt.show.call(this);
}
function setVmode() { function setVmode() {
var v = vid(); var v = vid();
ebi('bbox-vmode').style.display = v ? '' : 'none'; ebi('bbox-vmode').style.display = v ? '' : 'none';
@ -479,12 +450,10 @@ window.baguetteBox = (function () {
if (anymod(e)) if (anymod(e))
return; return;
var k = (e.key || e.code) + ''; var k = e.code + '';
if (k == "Space" || k == "Spacebar" || k == " ") { if (k == "Space")
un_pp = Date.now(); ev(e);
return ev(e);
}
} }
var passiveSupp = false; var passiveSupp = false;
@ -510,13 +479,12 @@ window.baguetteBox = (function () {
bind(document, 'fullscreenchange', onFSC); bind(document, 'fullscreenchange', onFSC);
bind(overlay, 'click', overlayClickHandler); bind(overlay, 'click', overlayClickHandler);
bind(overlay, 'wheel', overlayWheelHandler); bind(overlay, 'wheel', overlayWheelHandler);
bind(btnPrev, 'click', showLeftImage); bind(btnPrev, 'click', showPreviousImage);
bind(btnNext, 'click', showRightImage); bind(btnNext, 'click', showNextImage);
bind(btnClose, 'click', hideOverlay); bind(btnClose, 'click', hideOverlay);
bind(btnVmode, 'click', tglVmode); bind(btnVmode, 'click', tglVmode);
bind(btnHelp, 'click', halp); bind(btnHelp, 'click', halp);
bind(btnAnim, 'click', anim); bind(btnAnim, 'click', anim);
bind(btnReadDir, 'click', toggleReadDir);
bind(btnRotL, 'click', rotl); bind(btnRotL, 'click', rotl);
bind(btnRotR, 'click', rotr); bind(btnRotR, 'click', rotr);
bind(btnSel, 'click', tglsel); bind(btnSel, 'click', tglsel);
@ -534,13 +502,12 @@ window.baguetteBox = (function () {
unbind(document, 'fullscreenchange', onFSC); unbind(document, 'fullscreenchange', onFSC);
unbind(overlay, 'click', overlayClickHandler); unbind(overlay, 'click', overlayClickHandler);
unbind(overlay, 'wheel', overlayWheelHandler); unbind(overlay, 'wheel', overlayWheelHandler);
unbind(btnPrev, 'click', showLeftImage); unbind(btnPrev, 'click', showPreviousImage);
unbind(btnNext, 'click', showRightImage); unbind(btnNext, 'click', showNextImage);
unbind(btnClose, 'click', hideOverlay); unbind(btnClose, 'click', hideOverlay);
unbind(btnVmode, 'click', tglVmode); unbind(btnVmode, 'click', tglVmode);
unbind(btnHelp, 'click', halp); unbind(btnHelp, 'click', halp);
unbind(btnAnim, 'click', anim); unbind(btnAnim, 'click', anim);
unbind(btnReadDir, 'click', toggleReadDir);
unbind(btnRotL, 'click', rotl); unbind(btnRotL, 'click', rotl);
unbind(btnRotR, 'click', rotr); unbind(btnRotR, 'click', rotr);
unbind(btnSel, 'click', tglsel); unbind(btnSel, 'click', tglsel);
@ -591,23 +558,6 @@ window.baguetteBox = (function () {
btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)]; btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)];
btnAnim.setAttribute('tt', 'animation: ' + an); btnAnim.setAttribute('tt', 'animation: ' + an);
options.readDirRtl = sread('greaddir') === "rtl";
var msg;
if (options.readDirRtl) {
btnReadDir.innerText = "rtl";
msg = "browse from right to left";
slider.style.display = "flex";
slider.style.flexDirection = "row-reverse";
} else {
btnReadDir.innerText = "ltr";
msg = "browse from left to right";
slider.style.flexDirection = "";
slider.style.display = "block";
}
btnReadDir.setAttribute("tt", msg);
btnReadDir.setAttribute("aria-label", msg);
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .3s ease' : slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .3s ease' :
options.animation === 'slideIn' ? '' : 'none'); options.animation === 'slideIn' ? '' : 'none');
@ -828,7 +778,8 @@ window.baguetteBox = (function () {
image.setAttribute('playsinline', '1'); image.setAttribute('playsinline', '1');
// ios ignores poster // ios ignores poster
image.onended = vidEnd; image.onended = vidEnd;
image.onplay = image.onpause = ppHandler; image.onplay = function () { show_buttons(1); };
image.onpause = function () { show_buttons(); };
} }
image.alt = thumbnailElement ? thumbnailElement.alt || '' : ''; image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
if (options.titleTag && imageCaption) if (options.titleTag && imageCaption)
@ -843,33 +794,12 @@ window.baguetteBox = (function () {
callback(); callback();
} }
function ppHandler() { function showNextImage(e) {
var now = Date.now();
if (now - un_pp < 50) {
un_pp = 0;
return playpause(); // browser undid space hotkey
}
show_buttons(this.paused ? 1 : 0);
}
function showRightImage(e) {
ev(e);
var dir = options.readDirRtl ? -1 : 1;
return show(currentIndex + dir);
}
function showLeftImage(e) {
ev(e);
var dir = options.readDirRtl ? 1 : -1;
return show(currentIndex + dir);
}
function showNextImageIgnoreReadDir(e) {
ev(e); ev(e);
return show(currentIndex + 1); return show(currentIndex + 1);
} }
function showPreviousImageIgnoreReadDir(e) { function showPreviousImage(e) {
ev(e); ev(e);
return show(currentIndex - 1); return show(currentIndex - 1);
} }
@ -897,10 +827,10 @@ window.baguetteBox = (function () {
} }
if (index < 0) if (index < 0)
return bounceAnimation(options.readDirRtl ? 'right' : 'left'); return bounceAnimation('left');
if (index >= imagesElements.length) if (index >= imagesElements.length)
return bounceAnimation(options.readDirRtl ? 'left' : 'right'); return bounceAnimation('right');
try { try {
vid().pause(); vid().pause();
@ -1054,7 +984,7 @@ window.baguetteBox = (function () {
function vidEnd() { function vidEnd() {
if (this == vid() && vnext) if (this == vid() && vnext)
showNextImageIgnoreReadDir(); showNextImage();
} }
function setloop(side) { function setloop(side) {
@ -1115,12 +1045,11 @@ window.baguetteBox = (function () {
return false; return false;
} }
function updateOffset(noTransition) { function updateOffset() {
var dir = options.readDirRtl ? 1 : -1, var offset = -currentIndex * 100 + '%',
offset = dir * currentIndex * 100 + '%',
xform = slider.style.perspective !== undefined; xform = slider.style.perspective !== undefined;
if (options.animation === 'fadeIn' && !noTransition) { if (options.animation === 'fadeIn') {
slider.style.opacity = 0; slider.style.opacity = 0;
setTimeout(function () { setTimeout(function () {
xform ? xform ?
@ -1166,10 +1095,10 @@ window.baguetteBox = (function () {
fx = x / (rc.right - rc.left); fx = x / (rc.right - rc.left);
if (fx < 0.3) if (fx < 0.3)
return showLeftImage(); return showPreviousImage();
if (fx > 0.7) if (fx > 0.7)
return showRightImage(); return showNextImage();
show_buttons('t'); show_buttons('t');
@ -1225,8 +1154,8 @@ window.baguetteBox = (function () {
return { return {
run: run, run: run,
show: show, show: show,
showNext: showRightImage, showNext: showNextImage,
showPrevious: showLeftImage, showPrevious: showPreviousImage,
relseek: relseek, relseek: relseek,
urltime: urltime, urltime: urltime,
playpause: playpause, playpause: playpause,

View file

@ -84,13 +84,6 @@
--sort-1: #fb0; --sort-1: #fb0;
--sort-2: #d09; --sort-2: #d09;
--sz-b: #aaa;
--sz-k: #4ff;
--sz-m: var(--tab-alt);
--sz-g: var(--a);
--sz-t: var(--sz-g);
--sz-p: var(--sz-t);
--srv-1: #aaa; --srv-1: #aaa;
--srv-2: #a73; --srv-2: #a73;
--srv-3: #f4c; --srv-3: #f4c;
@ -194,9 +187,6 @@ html.y {
--sort-1: #059; --sort-1: #059;
--sort-2: #f5d; --sort-2: #f5d;
--sz-b: #777;
--sz-k: #380;
--srv-1: #555; --srv-1: #555;
--srv-2: #c83; --srv-2: #c83;
--srv-3: #c0a; --srv-3: #c0a;
@ -354,9 +344,6 @@ html.cz {
--btn-1-bb: .2em solid #e90; --btn-1-bb: .2em solid #e90;
--btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9); --btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9);
--sz-b: #ddd;
--sz-k: #c9f;
--srv-3: #fff; --srv-3: #fff;
--u2-tab-b1: var(--bg-d3); --u2-tab-b1: var(--bg-d3);
@ -584,7 +571,6 @@ pre, code, tt, #doc, #doc>code {
overflow: hidden; overflow: hidden;
width: 0; width: 0;
height: 0; height: 0;
left: -10em;
color: var(--bg); color: var(--bg);
} }
html .ayjump:focus { html .ayjump:focus {
@ -753,15 +739,6 @@ html.y #files tr.fade a {
#files tbody tr td:last-child { #files tbody tr td:last-child {
white-space: nowrap; white-space: nowrap;
} }
#files span.fsz_B { color: var(--sz-b); }
#files span.fsz_K { color: var(--sz-k); }
#files span.fsz_M { color: var(--sz-m); }
#files span.fsz_G { color: var(--sz-g); }
#files span.fsz_T { color: var(--sz-t); }
#files span.fsz_P { color: var(--sz-p); }
html.y #files span.fsz_G,
html.y #files span.fsz_T,
html.y #files span.fsz_P { font-weight: bold }
#files thead th[style] { #files thead th[style] {
width: auto !important; width: auto !important;
} }
@ -887,9 +864,6 @@ html.y #path a:hover {
#srv_info2 span { #srv_info2 span {
color: var(--srv-1); color: var(--srv-1);
} }
#srv_info2 a {
padding: 0;
}
#srv_info2 { #srv_info2 {
display: none; display: none;
} }
@ -907,9 +881,6 @@ html.y #path a:hover {
#flogout { #flogout {
display: inline; display: inline;
} }
html.dz #flogout {
margin-left: 1em;
}
#goh+span { #goh+span {
color: var(--bg-u5); color: var(--bg-u5);
padding-left: .5em; padding-left: .5em;
@ -1180,9 +1151,6 @@ html.y #widget.open {
border: 1px solid var(--bg-u5); border: 1px solid var(--bg-u5);
border-width: 0 .1em 0 0; border-width: 0 .1em 0 0;
} }
#wzip1 {
margin-right: .2em;
}
#wfm.act+#wzip1+#wzip, #wfm.act+#wzip1+#wzip,
#wfm.act+#wzip1+#wzip+#wnp { #wfm.act+#wzip1+#wzip+#wnp {
margin-left: .2em; margin-left: .2em;
@ -1586,13 +1554,11 @@ html {
#treepar { #treepar {
z-index: 1; z-index: 1;
position: fixed; position: fixed;
background: #fff;
background: var(--tree-bg); background: var(--tree-bg);
left: -.96em; left: -.96em;
width: calc(.3em + var(--nav-sz) - var(--sbw)); width: calc(.3em + var(--nav-sz) - var(--sbw));
border-bottom: 1px solid var(--bg-u5); border-bottom: 1px solid var(--bg-u5);
overflow: hidden; overflow: hidden;
border-right: .5em solid #999\9;
} }
#treepar.off { #treepar.off {
display: none; display: none;
@ -1939,8 +1905,6 @@ html.y #tree.nowrap .ntree a+a:hover {
#rui td input[type="text"] { #rui td input[type="text"] {
width: 100%; width: 100%;
} }
#rui #rn_n_d,
#rui #rn_n_s,
#shui td.exs input[type="text"] { #shui td.exs input[type="text"] {
width: 3em; width: 3em;
} }
@ -2030,7 +1994,6 @@ html.y #doc .line-highlight {
} }
#seldoc.sel { #seldoc.sel {
color: var(--fg2-max); color: var(--fg2-max);
background: #f0f;
background: var(--g-sel-b1); background: var(--g-sel-b1);
} }
#pvol, #pvol,
@ -2050,7 +2013,6 @@ a.btn,
user-select: none; user-select: none;
} }
#hkhelp { #hkhelp {
background: #fff;
background: var(--bg); background: var(--bg);
} }
#hkhelp table { #hkhelp table {
@ -2145,7 +2107,6 @@ html.noscroll .sbar::-webkit-scrollbar {
width: 100%; width: 100%;
height: 100%; height: 100%;
text-align: center; text-align: center;
flex: none;
} }
.full-image figure { .full-image figure {
display: inline; display: inline;
@ -2340,9 +2301,6 @@ html.y #bbox-overlay figcaption a {
0%, 100% {transform: scale(0)} 0%, 100% {transform: scale(0)}
50% {transform: scale(1)} 50% {transform: scale(1)}
} }
.no-transition {
transition: none !important;
}
@ -3530,6 +3488,7 @@ html.ez {
html.e { html.e {
text-shadow: none; text-shadow: none;
} }
html.e #files, html.e #files,
html.e #u2conf input[type="checkbox"]:hover + label, html.e #u2conf input[type="checkbox"]:hover + label,
html.e .tgl.btn.on:hover, html.e .tgl.btn.on:hover,
@ -3629,10 +3588,6 @@ html.e #srv_info {
display: flex; display: flex;
align-items: center; align-items: center;
} }
html.e #acc_info span.warn,
html.e #acc_info a {
color: var(--white);
}
html.e #flogout:before { html.e #flogout:before {
padding-left: 0.2em; padding-left: 0.2em;
padding-right: 0.4em; padding-right: 0.4em;

View file

@ -134,6 +134,7 @@
CGV = {{ cgv|tojson }}, CGV = {{ cgv|tojson }},
TS = "{{ ts }}", TS = "{{ ts }}",
dtheme = "{{ dtheme }}", dtheme = "{{ dtheme }}",
srvinf = "{{ srv_info }}",
lang = "{{ lang }}", lang = "{{ lang }}",
dfavico = "{{ favico }}", dfavico = "{{ favico }}",
have_tags_idx = {{ have_tags_idx }}, have_tags_idx = {{ have_tags_idx }},

File diff suppressed because it is too large Load diff

View file

@ -23,17 +23,17 @@
<th>user</th> <th>user</th>
<th>groups</th> <th>groups</th>
</tr></thead><tbody> </tr></thead><tbody>
{%- for un, gn in rows %} {% for un, gn in rows %}
<tr> <tr>
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td> <td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
<td>{{ un|e }}</td> <td>{{ un|e }}</td>
<td>{{ gn|e }}</td> <td>{{ gn|e }}</td>
</tr> </tr>
{%- endfor %} {% endfor %}
</tbody></table> </tbody></table>
{%- if not rows %} {% if not rows %}
(there are no IdP users in the cache) (there are no IdP users in the cache)
{%- endif %} {% endif %}
</div> </div>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>

View file

@ -130,8 +130,7 @@ write markdown (most html is 🙆 too)
var SR = "{{ r }}", var SR = "{{ r }}",
last_modified = {{ lastmod }}, last_modified = {{ lastmod }},
have_emp = {{ have_emp }}, have_emp = {{ "true" if have_emp else "false" }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}"; dfavico = "{{ favico }}";
var md_opt = { var md_opt = {

View file

@ -201,7 +201,7 @@ function convert_markdown(md_text, dest_dom) {
var marked_opts = { var marked_opts = {
//headerPrefix: 'h-', //headerPrefix: 'h-',
breaks: !md_no_br, breaks: true,
gfm: true gfm: true
}; };
@ -217,7 +217,7 @@ function convert_markdown(md_text, dest_dom) {
catch (ex) { catch (ex) {
if (IE) { if (IE) {
dest_dom.innerHTML = 'IE cannot into markdown ;_;'; dest_dom.innerHTML = 'IE cannot into markdown ;_;';
return false; return;
} }
if (ext) if (ext)
@ -344,8 +344,6 @@ function convert_markdown(md_text, dest_dom) {
} }
catch (ex) { } catch (ex) { }
}, 1); }, 1);
return true;
} }
@ -424,7 +422,7 @@ function init_toc() {
} }
} }
// highlight the correct toc items + scroll into view // hilight the correct toc items + scroll into view
function freshen_toclist() { function freshen_toclist() {
if (anchors.length == 0) if (anchors.length == 0)
return; return;

View file

@ -1,10 +1,6 @@
"use strict"; "use strict";
var sloc0 = '' + location,
dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0);
// server state // server state
var server_md = dom_src.value; var server_md = dom_src.value;
@ -701,14 +697,8 @@ function reLastIndexOf(txt, ptn, end) {
// table formatter // table formatter
function fmt_table(e) { function fmt_table(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
try { //dom_tbox.className = '';
fmt_table2();
}
catch (ex) {
return toast.err(7, 'table-format (CTRL-K) failed:\n' + ex);
}
}
function fmt_table2() {
var txt = dom_src.value, var txt = dom_src.value,
ofs = dom_src.selectionStart, ofs = dom_src.selectionStart,
//o0 = txt.lastIndexOf('\n\n', ofs), //o0 = txt.lastIndexOf('\n\n', ofs),
@ -940,30 +930,23 @@ var set_lno = (function () {
// hotkeys / toolbar // hotkeys / toolbar
(function () { (function () {
var keydown = function (e) { var keydown = function (ev) {
if (!e && window.event) { if (!ev && window.event) {
e = window.event; ev = window.event;
if (dev_fbw == 1) { if (dev_fbw == 1) {
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace'); toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
console.error('using window.event'); console.error('using window.event');
} }
} }
var k = (e.key || e.code) + '', var kc = ev.code || ev.keyCode || ev.which,
editing = document.activeElement == dom_src; editing = document.activeElement == dom_src;
if (k.startsWith('Key')) //console.log(ev.key, ev.code, ev.keyCode, ev.which);
k = k.slice(3); if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
var kl = k.toLowerCase();
if (dbg_kbd)
console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which);
if (ctrl(e) && kl == "s") {
save(); save();
return false; return false;
} }
if (k == "Escape" || k == "Esc") { if (ev.code == "Escape" || kc == 27) {
var d = ebi('helpclose'); var d = ebi('helpclose');
if (d) if (d)
d.click(); d.click();
@ -971,44 +954,46 @@ var set_lno = (function () {
if (editing) if (editing)
set_lno(); set_lno();
if (ctrl(e)) { if (ctrl(ev)) {
if (kl == "e") { if (ev.code == "KeyE") {
dom_nsbs.click(); dom_nsbs.click();
return false; return false;
} }
if (!editing) if (!editing)
return true; return true;
if (kl == "h") { if (ev.code == "KeyH" || kc == 72) {
md_header(e.shiftKey); md_header(ev.shiftKey);
return false; return false;
} }
if (kl == "z") { if (ev.code == "KeyZ" || kc == 90) {
if (e.shiftKey) if (ev.shiftKey)
action_stack.redo(); action_stack.redo();
else else
action_stack.undo(); action_stack.undo();
return false; return false;
} }
if (kl == "y") { if (ev.code == "KeyY" || kc == 89) {
action_stack.redo(); action_stack.redo();
return false; return false;
} }
if (kl == "k") { if (ev.code == "KeyK") {
fmt_table(); fmt_table();
return false; return false;
} }
if (kl == "u") { if (ev.code == "KeyU") {
iter_uni(); iter_uni();
return false; return false;
} }
if (k == "ArrowUp" || k == "ArrowDown") { var up = ev.code == "ArrowUp" || kc == 38;
md_p_jump(k == "ArrowDown"); var dn = ev.code == "ArrowDown" || kc == 40;
if (up || dn) {
md_p_jump(dn);
return false; return false;
} }
if (kl == "x" || kl == "c") { if (ev.code == "KeyX" || ev.code == "KeyC") {
md_cut(kl == "x"); md_cut(ev.code == "KeyX");
return true; //sic return true; //sic
} }
} }
@ -1016,18 +1001,18 @@ var set_lno = (function () {
if (!editing) if (!editing)
return true; return true;
if (k == "Tab") { if (ev.code == "Tab" || kc == 9) {
md_indent(e.shiftKey); md_indent(ev.shiftKey);
return false; return false;
} }
if (k == "Home") { if (ev.code == "Home" || kc == 36) {
md_home(e.shiftKey); md_home(ev.shiftKey);
return false; return false;
} }
if (!e.shiftKey && k.endsWith("Enter")) { if (!ev.shiftKey && ((ev.code + '').endsWith("Enter") || kc == 13)) {
return md_newline(); return md_newline();
} }
if (!e.shiftKey && k == "Backspace") { if (!ev.shiftKey && kc == 8) {
return md_backspace(); return md_backspace();
} }
} }
@ -1051,9 +1036,7 @@ ebi('help').onclick = function (e) {
var dom = ebi('helpbox'); var dom = ebi('helpbox');
var dtxt = dom.getElementsByTagName('textarea'); var dtxt = dom.getElementsByTagName('textarea');
if (dtxt.length > 0) { if (dtxt.length > 0) {
var txt = dtxt[0].value; convert_markdown(dtxt[0].value, dom);
if (!convert_markdown(txt, dom))
dom.innerText = txt.split('## markdown')[0];
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML; dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML;
} }

View file

@ -28,8 +28,7 @@
var SR = "{{ r }}", var SR = "{{ r }}",
last_modified = {{ lastmod }}, last_modified = {{ lastmod }},
have_emp = {{ have_emp }}, have_emp = {{ "true" if have_emp else "false" }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}"; dfavico = "{{ favico }}";
var md_opt = { var md_opt = {

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
{%- for d in dirs %}
<entry>
<title>{{ d.name }}</title>
<link rel="subsection"
href="{{ d.href | e }}"
type="application/atom+xml;profile=opds-catalog"/>
<updated>{{ d.iso8601 }}</updated>
</entry>
{%- endfor %}
{%- for f in files %}
<entry>
<title>{{ f.name }}</title>
<updated>{{ f.iso8601 }}</updated>
<link rel="http://opds-spec.org/acquisition"
href="{{ f.href | e }}"
type="{{ f.mime }}"/>
{%- if f.jpeg_thumb_href != None %}
<link rel="http://opds-spec.org/image/thumbnail"
href="{{ f.jpeg_thumb_href | e }}"
type="image/jpeg"/>
{%- endif %}
{%- if f.jpeg_thumb_href_hires != None %}
<link rel="http://opds-spec.org/image"
href="{{ f.jpeg_thumb_href_hires | e }}"
type="image/jpeg"/>
{%- endif %}
</entry>
{%- endfor %}
</feed>

View file

@ -36,7 +36,7 @@
<th>hrs</th> <th>hrs</th>
<th>add time</th> <th>add time</th>
</tr></thead><tbody> </tr></thead><tbody>
{%- for k, pw, vp, pr, st, un, t0, t1 in rows %} {% for k, pw, vp, pr, st, un, t0, t1 in rows %}
<tr> <tr>
<td> <td>
<a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a> <a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a>
@ -54,11 +54,11 @@
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td> <td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
<td></td> <td></td>
</tr> </tr>
{%- endfor %} {% endfor %}
</tbody></table> </tbody></table>
{%- if not rows %} {% if not rows %}
(you don't have any active shares btw) (you don't have any active shares btw)
{%- endif %} {% endif %}
</div> </div>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>

View file

@ -38,7 +38,6 @@ a {
td a { td a {
margin: 0; margin: 0;
} }
#wb,
#w { #w {
color: #fff; color: #fff;
background: #940; background: #940;
@ -118,26 +117,6 @@ table {
.btns>a:first-child { .btns>a:first-child {
margin-left: 0; margin-left: 0;
} }
.agr br {
display: none;
}
#lo,
.agr a,
.agr form {
margin: 0 .5em 0 0;
line-height: 4em;
}
.agr form,
.agr input {
display: inline;
padding: 0;
margin: 0;
}
#lo,
.agr input {
line-height: 1em;
font-weight: normal;
}
#msg { #msg {
margin: 3em 0; margin: 3em 0;
} }

View file

@ -21,11 +21,7 @@
{%- if this.uname == '*' %} {%- if this.uname == '*' %}
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p> <p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %} {%- else %}
{%- if this.args.idp_logout %}
<a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a>
{%- else %}
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
{%- endif %}
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p> <p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
{%- endif %} {%- endif %}
{%- endif %} {%- endif %}
@ -41,9 +37,9 @@
<table class="vols"> <table class="vols">
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead> <thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
<tbody> <tbody>
{%- for u in ups %} {% for u in ups %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr> <tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
{%- endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{%- endif %} {%- endif %}
@ -53,9 +49,9 @@
<table class="vols"> <table class="vols">
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead> <thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
<tbody> <tbody>
{%- for u in dls %} {% for u in dls %}
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr> <tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
{%- endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{%- endif %} {%- endif %}
@ -74,11 +70,11 @@
<table class="vols"> <table class="vols">
<thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead> <thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
<tbody> <tbody>
{%- for mp in avol %} {% for mp in avol %}
{%- if mp in vstate and vstate[mp] %} {%- if mp in vstate and vstate[mp] %}
<tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr> <tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
{%- endif %} {%- endif %}
{%- endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</td></tr></table> </td></tr></table>
@ -91,18 +87,18 @@
{%- if rvol %} {%- if rvol %}
<h1 id="f">you can browse:</h1> <h1 id="f">you can browse:</h1>
<ul> <ul>
{%- for mp in rvol %} {% for mp in rvol %}
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li> <li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{%- endfor %} {% endfor %}
</ul> </ul>
{%- endif %} {%- endif %}
{%- if wvol %} {%- if wvol %}
<h1 id="g">you can upload to:</h1> <h1 id="g">you can upload to:</h1>
<ul> <ul>
{%- for mp in wvol %} {% for mp in wvol %}
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li> <li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
{%- endfor %} {% endfor %}
</ul> </ul>
{%- endif %} {%- endif %}
@ -114,85 +110,71 @@
<input type="password" id="lp" name="cppwd" placeholder=" password" /> <input type="password" id="lp" name="cppwd" placeholder=" password" />
<input type="hidden" name="uhash" id="uhash" value="x" /> <input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="Unlock" /> <input type="submit" id="ls" value="Unlock" />
{%- if ahttps %} {% if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a> <a id="w" href="{{ ahttps }}">switch to https</a>
{%- endif %} {% endif %}
</form> </form>
</div> </div>
{%- else %} {%- else %}
<h1 id="l">login for more:</h1> <h1 id="l">login for more:</h1>
<div> <div>
{%- if this.args.idp_login %}
<ul><li>
<a href="{{ this.args.idp_login | replace("{dst}",r+"/"+qvpath) }}">{{ this.args.idp_login_t }}</a>
{%- if this.args.ao_have_pw %}or alternatively:{%- endif %}
</li></ul>
{%- endif %}
{%- if this.args.ao_have_pw %}
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}"> <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" /> <input type="hidden" id="la" name="act" value="login" />
{%- if this.args.usernames %} {% if this.args.usernames %}
<input type="text" id="lu" name="uname" placeholder=" username" size="12" /> <input type="text" id="lu" name="uname" placeholder=" username" size="12" />
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" /> <input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
{%- else %} {% else %}
<input type="password" id="lp" name="cppwd" placeholder=" password" /> <input type="password" id="lp" name="cppwd" placeholder=" password" />
{%- endif %} {% endif %}
<input type="hidden" name="uhash" id="uhash" value="x" /> <input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="login" /> <input type="submit" id="ls" value="login" />
{%- if chpw %} {% if chpw %}
<a id="x" href="#">change password</a> <a id="x" href="#">change password</a>
{%- endif %} {% endif %}
{%- if ahttps %} {% if ahttps %}
<a id="w" href="{{ ahttps }}">switch to https</a> <a id="w" href="{{ ahttps }}">switch to https</a>
{%- endif %} {% endif %}
</form> </form>
{%- endif %}
</div> </div>
{%- endif %} {%- endif %}
<h1 id="cc">other stuff:</h1> <h1 id="cc">other stuff:</h1>
<div class="agr"> <ul>
{%- if ahttps %} {%- if this.uname in this.args.idp_adm_set %}
<a id="wb" href="{{ ahttps }}">switch to https</a><br /> <li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{%- endif %} {% endif %}
<a id="af" href="{{ r }}/?ru">show recent uploads</a><br />
{%- if this.uname != '*' and this.args.shr %} {%- if this.uname != '*' and this.args.shr %}
<a id="y" href="{{ r }}/?shares">edit shares</a><br /> <li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{%- endif %} {% endif %}
{%- if this.uname in this.args.idp_adm_set %} {% if k304 or k304vis %}
<a id="ag" href="{{ r }}/?idp">view idp cache</a><br /> {% if k304 %}
{%- endif %}
<a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a><br />
{%- if this.uname != '*' and not in_shr %}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form>
{%- endif %}
</div>
<ul>
{%- if k304 or k304vis %}
{%- if k304 %}
<li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled) <li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
{%- else %} {%- else %}
<li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled) <li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
{%- endif %} {% endif %}
<blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li> <blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
{%- endif %} {% endif %}
{%- if no304 or no304vis %} {% if no304 or no304vis %}
{%- if no304 %} {% if no304 %}
<li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled) <li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
{%- else %} {%- else %}
<li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled) <li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
{%- endif %} {% endif %}
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li> <blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
{%- endif %} {% endif %}
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
{%- if this.uname != '*' %}
<li><form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form></li>
{% endif %}
</ul> </ul>
</div> </div>

View file

@ -197,53 +197,6 @@ var Ls = {
"ae1": "Aktive Downloads:", "ae1": "Aktive Downloads:",
"af1": "Zeige neue Uploads", "af1": "Zeige neue Uploads",
}, },
"epo": {
"a1": "reŝargi",
"b1": "sal, nekonatulo &nbsp; <small>(vi ne estas ensalutita)</small>",
"c1": "elsaluti",
"d1": "montru stakon", // TLNote: "d2" is the tooltip for this button
"d2": "montras la staton de ĉiuj aktivaj fadenoj",
"e1": "reŝargi CFGon",
"e2": "reŝargas la agordo-dosierojn (kontoj/portiloj/portilo-flagoj),$Nkaj reskanas ĉiuj portiloj de ed2s$N$Nnoto: ĉiuj ŝanĝoj de ĝeneralaj agordoj postulas$Npostulas tutan restartigon por efektiviĝi",
"f1": "vi povas vidi:",
"g1": "vi povas alŝuti al:",
"cc1": "aliaĵoj:",
"h1": "malŝalti k304-on", // TLNote: "j1" explains what k304 is
"i1": "ŝalti k304-on",
"j1": "k304 malkonektas vian klienton je ĉiu HTTP-eraro 304; tio povas eviti paraliziĝon dum uzado de difektitaj retperantoj (paĝoj subite ne ŝargiĝas), <em>sed</em> ĝi ankaŭ plimalrapidigas ĉion",
"k1": "rekomenci agordojn de kliento",
"l1": "ensaluti por pli da opcioj:",
"ls3": "ensaluti", //m
"lu4": "uzantnomo", //m
"lp4": "pasvorto", //m
"lo3": "ensaluti kiel “{0}” ĉie", //m
"lo2": "ĉi tiu finigos seancon en ĉiuj retumiloj", //m
"m1": "bonvenon denove,", // TLNote: "welcome back, USERNAME"
"n1": "404 ne trovita &nbsp;┐( ´ -`)┌",
"o1": 'aŭ eble vi ne havas rajton -- provu uzi pasvorton aŭ <a href="' + SR + '/?h">iri hejmen</a>',
"p1": "403 ne permesita &nbsp;~┻━┻",
"q1": 'uzu pasvorton aŭ <a href="' + SR + '/?h">iru hejmen</a>',
"r1": "hejmen",
".s1": "reskani",
"t1": "ago", // TLNote: this is the header above the "rescan" buttons
"u2": "tempo post lasta skribo (alŝuto / alinomado / ...) je servilo$N( upload / rename / ... )$N$N17d = 17 tagoj$N1h23 = 1 horo 23 minutoj$N4m56 = 4 minutoj 56 sekundoj",
"v1": "konekti",
"v2": "uzi ĉi tiun servilon kiel loka disko",
"w1": "uzi HTTPS-protokolon",
"x1": "ŝanĝi pasvorton",
"y1": "redakti komunaĵojn", // TLNote: shows the list of folders that the user has decided to share
"z1": "malŝlosi ĉi tiun komunaĵon:", // TLNote: the password prompt to see a hidden share
"ta1": "entajpu novan pasvorton unue",
"ta2": "retajpu por konfirmi:",
"ta3": "tajpo-eraro; bonvolu provu denove",
"aa1": "aktivaj alŝutoj:",
"ab1": "malŝalti no304-on",
"ac1": "ŝalti no304-on",
"ad1": "no304 malŝaltas ĉiun kaŝmemoradon; provu ĉi tion, se k304 ne riparis la difektojn. Ĉi tiu agordo malŝparas multon da datumtrafiko!",
"ae1": "aktivaj elŝutoj:",
"af1": "montri lastajn alŝutojn",
"ag1": "montri kaŝmemoron de idp",
},
"fin": { "fin": {
"a1": "päivitä", "a1": "päivitä",
"b1": "hei sie muukalainen &nbsp; <small>(et ole kirjautunut sisään)</small>", "b1": "hei sie muukalainen &nbsp; <small>(et ole kirjautunut sisään)</small>",
@ -618,53 +571,6 @@ var Ls = {
"af1": "pokaż ostatnio przesłane pliki", "af1": "pokaż ostatnio przesłane pliki",
"ag1": "pokaż znanych użytkowników IdP", "ag1": "pokaż znanych użytkowników IdP",
}, },
"por": {
"a1": "atualizar",
"b1": "olá &nbsp; <small>(você não está logado)</small>",
"c1": "encerrar sessão",
"d1": "despejar o estado da pilha",
"d2": "mostra o estado de todos os threads ativos",
"e1": "recarregar configuração",
"e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito",
"f1": "você pode navegar:",
"g1": "você pode fazer upload para:",
"cc1": "outras coisas:",
"h1": "desativar k304",
"i1": "ativar k304",
"j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral",
"k1": "redefinir config. de cliente",
"l1": "faça login para mais:",
"ls3": "fazer login",
"lu4": "nome de usuário",
"lp4": "senha",
"lo3": "encerrar sessão de \"{0}\" em todos os lugares",
"lo2": "isso irá encerrar a sessão em todos os navegadores",
"m1": "bem-vindo de volta,",
"n1": "404 não encontrado &nbsp;┐( ´ -`)┌",
"o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início",
"p1": "403 proibido &nbsp;~┻━┻",
"q1": "use uma senha ou volte para o início",
"r1": "ir para o início",
".s1": "reescanear",
"t1": "ação",
"u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
"v1": "conectar",
"v2": "usar este servidor como um disco rígido local",
"w1": "mudar para https",
"x1": "mudar senha",
"y1": "editar recursos compartilhados",
"z1": "desbloquear este recurso compartilhado:",
"ta1": "primeiro digite sua nova senha",
"ta2": "repita para confirmar a nova senha:",
"ta3": "há um erro; por favor, tente novamente",
"aa1": "arquivos de entrada:",
"ab1": "desativar no304",
"ac1": "ativar no304",
"ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!",
"ae1": "downloads ativos:",
"af1": "mostrar uploads recentes",
"ag1": "mostrar usuários IdP conhecidos"
},
"spa": { "spa": {
"a1": "actualizar", "a1": "actualizar",
"b1": "hola &nbsp; <small>(no has iniciado sesión)</small>", "b1": "hola &nbsp; <small>(no has iniciado sesión)</small>",
@ -853,47 +759,6 @@ var Ls = {
"af1": "показать недавние загрузки", "af1": "показать недавние загрузки",
"ag1": "показать известных IdP-пользователей", "ag1": "показать известных IdP-пользователей",
}, },
"tur": {
"a1": "yenile",
"b1": "N'aber aga &nbsp; <small>(giriş yapmamışsın)</small>",
"c1": ıkış yap",
"d1": "yığını yolla",
"d2": "tüm aktif iş parçacıklarının durumunu gösterir",
"e1": "cfg'yi yenile",
"e2": "yapılandırma dosyalarını yenile (hesaplar/hacimler/hacim bayrakları),$Nve tüm e2ds hacimlerini yeniden tarayın$N$Nnot: global ayarlardaki herhangi bir değişiklik$Netkili hale gelmesi için tam bir yeniden başlatma gerektirir",
"f1": "göz atabilirsiniz:",
"g1": "yükleyebilirsiniz:",
"cc1": "diğer şeyler:",
"h1": "k304'ü devre dışı bırak",
"i1": "k304'ü etkinleştir",
"j1": "k304'ü etkinleştirmek, her HTTP 304'te istemcinizin bağlantısını keser, bu da bazı hatalı proxy'lerin takılmasını önleyebilir (sayfaların birdenbire yüklenmesinin durması gibi); <em>ama</em> bu, aynı zamanda genel olarak işleyişi yavaşlatır",
"k1": "istemci ayarlarını sıfırla",
"l1": "daha fazlası için giriş yap:",
"m1": "hoş geldin,",
"n1": "404 bulunamadı &nbsp;┐( ´ -`)┌",
"o1": 'ya da erişim iznin yok -- bir şifre dene veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
"p1": "403 yasaklandı &nbsp;~┻━┻",
"q1": 'bir şifre kullan veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
"r1": "ana sayfaya dön",
".s1": "yeniden tara",
"t1": "işlem",
"u2": "son sunucu yazma zamanı$N( yükleme / yeniden adlandırma / ... )$N$N17d = 17 gün$N1h23 = 1 saat 23 dakika$N4m56 = 4 dakika 56 saniye",
"v1": "bağlan",
"v2": "bu sunucuyu yerel HDD olarak kullan",
"w1": "https'ye geç",
"x1": "şifreyi değiştir",
"y1": "paylaşılanları düzenle",
"z1": "gizli paylaşımın kilidini aç:",
"ta1": "ilk önce yeni şifreyi doldur",
"ta2": "yeni şifreyi onaylamak için tekrar girin:",
"ta3": "bir yazım hatası bulundu; lütfen tekrar deneyin",
"aa1": "gelen dosyalar:",
"ab1": "no304'ü devre dışı bırak",
"ac1": "no304'ü etkinleştir",
"ad1": "no304'ü etkinleştirmek, tüm önbelleği devre dışı bırakır; bunu k304 yeterli olmadıysa deneyin. Bu, büyük miktarda ağ trafiği israf edecektir!",
"ae1": "aktif indirmeler:",
"af1": "son yüklemeleri göster",
},
}; };
if (window.langmod) if (window.langmod)
@ -902,8 +767,6 @@ if (window.langmod)
var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] || var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] ||
Ls.eng || Ls.nor || Ls.chi; Ls.eng || Ls.nor || Ls.chi;
d.wb = d.w;
for (var k in (d || {})) { for (var k in (d || {})) {
var f = k.slice(-1), var f = k.slice(-1),
i = k.slice(0, -1), i = k.slice(0, -1),
@ -924,7 +787,7 @@ if (o1 && o2 && d.lo3)
o1.setAttribute("value", d.lo3.format(o2.textContent)); o1.setAttribute("value", d.lo3.format(o2.textContent));
try { try {
if (is_idp > 1) { if (is_idp) {
var z = ['#l+div', '#l', '#c']; var z = ['#l+div', '#l', '#c'];
for (var a = 0; a < z.length; a++) for (var a = 0; a < z.length; a++)
QS(z[a]).style.display = 'none'; QS(z[a]).style.display = 'none';
@ -934,16 +797,14 @@ catch (ex) { }
tt.init(); tt.init();
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]'); var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
if (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus(); o.focus();
o = ebi('u'); o = ebi('u');
if (o && /[0-9]+$/.exec(o.innerHTML)) if (o && /[0-9]+$/.exec(o.innerHTML))
o.innerHTML = shumantime(o.innerHTML); o.innerHTML = shumantime(o.innerHTML);
o = ebi('uhash') ebi('uhash').value = '' + location.hash;
if (o)
o.value = '' + location.hash;
if (/\&re=/.test('' + location)) if (/\&re=/.test('' + location))
ebi('a').className = 'af g'; ebi('a').className = 'af g';

View file

@ -31,10 +31,10 @@
<br /> <br />
<span class="os win lin mac">placeholders:</span> <span class="os win lin mac">placeholders:</span>
<span class="os win"> <span class="os win">
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint {% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
</span> </span>
<span class="os lin mac"> <span class="os lin mac">
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint {% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</span> </span>
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %} {% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
<a href="#" id="qr">show qr</a> <a href="#" id="qr">show qr</a>
@ -44,7 +44,7 @@
{% if args.have_idp_hdrs %} {% if args.have_idp_hdrs %}
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p> <p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
{%- endif %} {% endif %}
@ -54,64 +54,50 @@
<div class="os win"> <div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p> <p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre> <pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %} rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre> </pre>
<ul> <ul>
{%- if s %} {% if s %}
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li> <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{%- endif %} {% endif %}
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li> <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul> </ul>
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p> <p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
<pre> <pre>
{%- if un %}
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}
{%- else %}
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %} net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
{%- endif %}
</pre> </pre>
</div> </div>
<div class="os lin"> <div class="os lin">
<p>rclone (v1.63 or later) is recommended:</p> <p>rclone (v1.63 or later) is recommended:</p>
<pre> <pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %} rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre> </pre>
<ul> <ul>
{%- if s %} {% if s %}
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li> <li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
{%- endif %} {% endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li> <li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li> <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul> </ul>
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p> <p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
<pre> <pre>
yum install davfs2 yum install davfs2
{%- if un %}
{% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
{%- else %}
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> {% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
{%- endif %}
</pre> </pre>
{%- if accs %}
<p>make davfs2 automount on boot:</p> <p>make davfs2 automount on boot:</p>
<pre> <pre>
{%- if un %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets
{%- else %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
{%- endif %}
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
</pre> </pre>
{%- endif %}
<p>or the emergency alternative (gnome/gui-only):</p> <p>or the emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp --> <!-- gnome-bug: ignores vp -->
<pre> <pre>
{%- if accs %} {%- if accs %}
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }} echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }}
{%- else %} {%- else %}
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }} gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
{%- endif %} {%- endif %}
@ -121,18 +107,18 @@
<div class="os mac"> <div class="os mac">
<pre> <pre>
osascript -e ' mount volume "http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" ' osascript -e ' mount volume "http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
</pre> </pre>
<p>or you can open up a Finder, press command-K and paste this instead:</p> <p>or you can open up a Finder, press command-K and paste this instead:</p>
<pre> <pre>
http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }} http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
</pre> </pre>
{%- if s %} {% if s %}
<p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p> <p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>
{%- endif %} {% endif %}
</div> </div>
{%- endif %} {% endif %}
@ -141,71 +127,51 @@
<div class="os win"> <div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p> <p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
{%- if args.ftp %} {% if args.ftp %}
<p>connect with plaintext FTP:</p> <p>connect with plaintext FTP:</p>
<pre> <pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
</pre> </pre>
{%- endif %} {% endif %}
{%- if args.ftps %} {% if args.ftps %}
<p>connect with TLS-encrypted FTPS:</p> <p>connect with TLS-encrypted FTPS:</p>
<pre> <pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre> </pre>
{%- endif %} {% endif %}
<ul> <ul>
{%- if args.ftps %} {% if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li> <li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{%- endif %} {% endif %}
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li> <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul> </ul>
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p> <p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
<pre> <pre>
{%- if un %}
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- else %}
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- endif %}
</pre> </pre>
</div> </div>
<div class="os lin"> <div class="os lin">
{%- if args.ftp %} {% if args.ftp %}
<p>connect with plaintext FTP:</p> <p>connect with plaintext FTP:</p>
<pre> <pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
</pre> </pre>
{%- endif %} {% endif %}
{%- if args.ftps %} {% if args.ftps %}
<p>connect with TLS-encrypted FTPS:</p> <p>connect with TLS-encrypted FTPS:</p>
<pre> <pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b> rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre> </pre>
{%- endif %} {% endif %}
<ul> <ul>
{%- if args.ftps %} {% if args.ftps %}
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li> <li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
{%- endif %} {% endif %}
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li> <li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
<li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li> <li>old version of rclone? replace all <code>=</code> with <code>&nbsp;</code> (space)</li>
</ul> </ul>
@ -213,7 +179,7 @@
<!-- gnome-bug: ignores vp --> <!-- gnome-bug: ignores vp -->
<pre> <pre>
{%- if accs %} {%- if accs %}
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- else %} {%- else %}
gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
{%- endif %} {%- endif %}
@ -223,10 +189,10 @@
<div class="os mac"> <div class="os mac">
<p>note: FTP is read-only on macos; please use WebDAV instead</p> <p>note: FTP is read-only on macos; please use WebDAV instead</p>
<pre> <pre>
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }} open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
</pre> </pre>
</div> </div>
{%- endif %} {% endif %}
@ -238,11 +204,11 @@
<span class="os lin">doesn't need root</span> <span class="os lin">doesn't need root</span>
</p> </p>
<pre> <pre>
partyfuse.py{% if accs %} -a <b>{{ unpw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b> partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
</pre> </pre>
{%- if s %} {% if s %}
<ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul> <ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>
{%- endif %} {% endif %}
<p> <p>
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers) you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)
</p> </p>
@ -251,10 +217,6 @@
{% if args.smb %} {% if args.smb %}
<h1>SMB / CIFS</h1> <h1>SMB / CIFS</h1>
{%- if un %}
<h2>not available on this server because <code>--usernames</code> is enabled in the server config</h2>
{%- else %}
<div class="os win"> <div class="os win">
<pre> <pre>
net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %} net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
@ -272,8 +234,7 @@
<pre class="os mac"> <pre class="os mac">
open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a' open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a'
</pre> </pre>
{%- endif %} {% endif %}
{%- endif %}
@ -286,7 +247,7 @@
{ "Version": "15.0.0", "Name": "copyparty", { "Version": "15.0.0", "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": { "Headers": {
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url" {% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
}, },
"DestinationType": "ImageUploader, TextUploader, FileUploader", "DestinationType": "ImageUploader, TextUploader, FileUploader",
"Body": "MultipartFormData", "URL": "{response}", "Body": "MultipartFormData", "URL": "{response}",
@ -299,7 +260,7 @@
{ "Name": "copyparty", { "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": { "Headers": {
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url" {% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
}, },
"DestinationType": "ImageUploader, TextUploader, FileUploader", "DestinationType": "ImageUploader, TextUploader, FileUploader",
"FileFormName": "f" } "FileFormName": "f" }
@ -317,9 +278,7 @@
{ "Name": "copyparty", { "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": { "Headers": {
{%- if accs %} {% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
"pw": "<b>{{ unpw }}</b>",
{%- endif %}
"accept": "json" "accept": "json"
}, },
"ResponseURL": "{{ '{{fileurl}}' }}", "ResponseURL": "{{ '{{fileurl}}' }}",
@ -336,7 +295,7 @@
<pre class="dl" name="flameshot.sh"> <pre class="dl" name="flameshot.sh">
#!/bin/bash #!/bin/bash
pw="<b>{{ unpw }}</b>" pw="<b>{{ pw }}</b>"
url="http{{ s }}://{{ ep }}/{{ rvp }}" url="http{{ s }}://{{ ep }}/{{ rvp }}"
filename="$(date +%Y-%m%d-%H%M%S).png" filename="$(date +%Y-%m%d-%H%M%S).png"
flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib

View file

@ -49,47 +49,21 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk'); setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
var un, un0, pw, pw0, unpw, up0; var pw = '';
function setpw(e) { function setpw(e) {
ev(e); ev(e);
if (!ebi('un0'))
return askpw();
modal.prompt('username:', '', function (v) {
if (!v)
return;
un = v;
un0 = ebi('un0').innerHTML;
var oa = QSA('b');
for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == un0)
oa[a].textContent = un;
askpw();
});
}
function askpw() {
modal.prompt('password:', '', function (v) { modal.prompt('password:', '', function (v) {
if (!v) if (!v)
return; return;
pw = v; pw = v;
pw0 = ebi('pw0').innerHTML; var pw0 = ebi('pw0').innerHTML,
var oa = QSA('b'); oa = QSA('b');
for (var a = 0; a < oa.length; a++) for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == pw0) if (oa[a].innerHTML == pw0)
oa[a].textContent = pw; oa[a].textContent = v;
if (un) {
unpw = un ? (un+':'+pw) : pw;
up0 = ebi('up0').innerHTML;
for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == up0)
oa[a].textContent = unpw;
}
add_dls(); add_dls();
}); });
} }

View file

@ -430,15 +430,6 @@ html.y textarea:focus {
.mdo code { .mdo code {
font-size: .96em; font-size: .96em;
} }
html.z .mdo a>code,
html.y .mdo a>code {
color: inherit;
background: inherit;
background: rgba(0, 0, 0, 0.2);
padding-top: 0;
padding-bottom: 0;
border: none;
}
.mdo h1, .mdo h1,
.mdo h2 { .mdo h2 {
line-height: 1.5em; line-height: 1.5em;

View file

@ -50,7 +50,7 @@ catch (ex) {
} }
catch (ex) { catch (ex) {
console.log('up2k init failed:', ex); console.log('up2k init failed:', ex);
toast.err(10, 'could not initialize up2k\n\n' + basenames(ex)); toast.err(10, 'could not initialze up2k\n\n' + basenames(ex));
} }
} }
treectl.onscroll(); treectl.onscroll();
@ -732,7 +732,7 @@ function Donut(uc, st) {
tstrober = setInterval(strobe, 300); tstrober = setInterval(strobe, 300);
if (uc.upsfx && actx && actx.state != 'suspended') if (uc.upsfx && actx && actx.state != 'suspended')
sfx_nice(); sfx();
// firefox may forget that filedrops are user-gestures so it can skip this: // firefox may forget that filedrops are user-gestures so it can skip this:
if (uc.upnag && Notification && Notification.permission == 'granted') if (uc.upnag && Notification && Notification.permission == 'granted')
@ -745,10 +745,8 @@ function Donut(uc, st) {
if (!txt) if (!txt)
clearInterval(tstrober); clearInterval(tstrober);
} }
}
function sfx_nice() { function sfx() {
if (true) {
var osc = actx.createOscillator(), var osc = actx.createOscillator(),
gain = actx.createGain(), gain = actx.createGain(),
gg = gain.gain, gg = gain.gain,
@ -1803,7 +1801,7 @@ function up2k_init(subtle) {
while (true) { while (true) {
var now = Date.now(), var now = Date.now(),
blocktime = now - r.tact, blocktime = now - r.tact,
was_busy = !!st.is_busy, was_busy = st.is_busy,
is_busy = !!( // gzip take the wheel is_busy = !!( // gzip take the wheel
st.car < st.files.length || st.car < st.files.length ||
st.busy.hash.length || st.busy.hash.length ||
@ -3039,12 +3037,10 @@ function up2k_init(subtle) {
if (anymod(e)) if (anymod(e))
return; return;
var k = e.key || e.code; if (e.code == 'ArrowUp')
if (k == 'ArrowUp')
bumpthread(1); bumpthread(1);
if (k == 'ArrowDown') if (e.code == 'ArrowDown')
bumpthread(-1); bumpthread(-1);
} }
@ -3107,8 +3103,7 @@ function up2k_init(subtle) {
ebi('u2szg').addEventListener('blur', read_u2sz); ebi('u2szg').addEventListener('blur', read_u2sz);
ebi('u2szg').onkeydown = function (e) { ebi('u2szg').onkeydown = function (e) {
if (anymod(e)) return; if (anymod(e)) return;
var k = e.key || e.code, var n = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
if (!n) return; if (!n) return;
this.value = parseInt(this.value) + n; this.value = parseInt(this.value) + n;
read_u2sz(); read_u2sz();
@ -3185,8 +3180,7 @@ function up2k_init(subtle) {
function kd_life(e) { function kd_life(e) {
var el = e.target, var el = e.target,
k = e.key || e.code, d = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
if (anymod(e) || !d) if (anymod(e) || !d)
return; return;
@ -3427,8 +3421,6 @@ if (QS('#op_up2k.act'))
goto_up2k(); goto_up2k();
apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts }); apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts });
if (ls0)
fileman.render();
(function () { (function () {

View file

@ -960,95 +960,15 @@ function f2f(val, nd) {
} }
var HSZ_U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
function humansize(b, terse) { function humansize(b, terse) {
var i = 0; var i = 0, u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
while (b >= 1000 && i < 5) { b /= 1024; i += 1; } while (b >= 1000 && i < u.length - 1) {
b /= 1024;
i += 1;
}
return (f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + return (f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) +
' ' + (terse ? HSZ_U[i].charAt(0) : HSZ_U[i])); ' ' + (terse ? u[i].charAt(0) : u[i]));
} }
function humansize_su(b) {
var i = 0;
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
return [b, HSZ_U[i]];
}
function humansize_0(b) {
return '' + b;
}
function humansize_1(b) {
return ('' + b).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function humansize_2g(b) {
var z = humansize_su(b), u = z[1].charAt(0); b = z[0];
return [f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u, u];
}
function humansize_3g(b) {
var z = humansize_su(b), u = z[1].charAt(0); b = z[0];
return [f2f(b, b >= 10 ? 0 : 1) + ' ' + u, u];
}
function humansize_4g(b) {
var z = humansize_su(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 100 ? 0 : b >= 10 ? 1 : 2)) + ' ' + u, u.charAt(0)];
}
function humansize_5g(b) {
var z = humansize_su(b), u = z[1]; b = z[0];
return [parseFloat(b.toFixed(b >= 10 ? 0 : 1)) + ' ' + u, u.charAt(0)];
}
function humansize_2(b) {
return humansize_2g(b)[0];
}
function humansize_3(b) {
return humansize_3g(b)[0];
}
function humansize_4(b) {
return humansize_4g(b)[0];
}
function humansize_5(b) {
return humansize_5g(b)[0];
}
function humansize_2c(b) {
var v = humansize_2g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_3c(b) {
var v = humansize_3g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_4c(b) {
var v = humansize_4g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_5c(b) {
var v = humansize_5g(b);
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
}
function humansize_fuzzy(b) {
if (b <= 0) return "yes";
if (b <= 80) return "hullkort";
if (b <= 368640) return "5¼ DD";
if (b <= 1474560) return "save icon";
if (b <= 2880000) return "3½ Extended";
if (b <= 13107200) return "C90 Tape";
if (b <= 21000000) return "Floptical";
if (b <= 33554432) return "MPMan F10";
if (b <= 50000000) return "creditcardCD";
if (b <= 100663296) return "Zipdisk";
if (b <= 170000000) return "MD";
if (b <= 220200960) return "8cm CD";
if (b <= 737280000) return "CD-R";
if (b <= 900000000) return "UMD";
if (b <= 1300000000) return "GD-ROM";
if (b <= 4700000000) return "DVD";
if (b <= 9400000000) return "DVD-DL";
if (b <= 25025000000) return "BluRei";
if (b <= 50050000000) return "BD-DL";
return "LTO";
}
var humansize_fmts = ['0', '1', '2', '2c', '3', '3c', '4', '4c', '5', '5c', 'fuzzy'];
window.filesizefun = (function () {
var v = sread('fszfmt', humansize_fmts);
return window['humansize_' + (v || window.dfszf)] || humansize_1;
})();
function humantime(v) { function humantime(v) {
@ -1904,12 +1824,12 @@ var modal = (function () {
}; };
var onkey = function (e) { var onkey = function (e) {
var k = (e.key || e.code) + '', var k = (e.code || e.key) + '',
eok = ebi('modal-ok'), eok = ebi('modal-ok'),
eng = ebi('modal-ng'), eng = ebi('modal-ng'),
ae = document.activeElement; ae = document.activeElement;
if ((k == 'Space' || k == 'Spacebar' || k == ' ') && ae && (ae === eok || ae === eng)) if (k == 'Space' && ae && (ae === eok || ae === eng))
k = 'Enter'; k = 'Enter';
if (k.endsWith('Enter')) { if (k.endsWith('Enter')) {
@ -2261,19 +2181,6 @@ function bchrome() {
} }
bchrome(); bchrome();
var XC_CMSG = {
502: "bad gateway (server offline)",
503: "server offline",
504: "gateway timeout (server busy)",
529: "gateway timeout (server busy)",
520: "unknown error from server",
521: "server offline",
523: "server offline",
522: "proxy timeout (server busy)",
524: "proxy timeout (server busy)",
598: "proxy timeout (server busy)",
599: "proxy timeout (server busy)",
};
var cf_cha_t = 0; var cf_cha_t = 0;
function xhrchk(xhr, prefix, e404, lvl, tag) { function xhrchk(xhr, prefix, e404, lvl, tag) {
if (xhr.status < 400 && xhr.status >= 200) if (xhr.status < 400 && xhr.status >= 200)
@ -2316,8 +2223,5 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
document.body.appendChild(fr); document.body.appendChild(fr);
} }
if (XC_CMSG[xhr.status] && (errtxt.indexOf('<html') + 1))
errtxt = XC_CMSG[xhr.status];
return fun(0, prefix + xhr.status + ": " + errtxt, tag); return fun(0, prefix + xhr.status + ": " + errtxt, tag);
} }

View file

@ -1,363 +1,3 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0929-2310 `v1.19.15` merry christmas
## 🧪 new features
* #184 add various human-readable formats for filesizes 234eddec
* search for files by their identifier ("wark"/checksum) 4e38e408
* and those are displayed in file-listings now too 456addf2
* PUT-upload with header `Replace` will overwrite any existing files 397ed565
* xbu/xau hooks can reject uploads with a custom message df0fa9d1
* #855 mDNS options to change the announced http/https port a3d95067
* #473 #383 custom favicons per-volume (.ico/png/gif/svg) 470b5048
* doesn't seem to work in internet explorer... ah whatever, go next
## 🩹 bugfixes
* #849 create IdP-db for `--idp-store` when necessary 80ca7851
* #859 cbz-thumbnailing had an accidental dependency on FFmpeg 983865d9
* docs: misleading markdown-expansion example e187df28
## 🔧 other changes
* #851 show a huge warning when copyparty accidentally detects a failing HDD and/or filesystem-corruption during indexing 6912e867 eb5d767b
* #870 improved discord video embeds (thx @tsuza!) f0ecb083
* #858 prefer reflinks (not hardlinks) in the `-ss` security option 57650a21
* improved controlpanel action-buttons layout 9f46e4db
## 🌠 fun facts
* includes (a tiny bit of) code written at [koie ramen](https://a.ocv.me/pub/g/2025/09/PXL_20250925_151716836.jpg)
* [according to Biltema](https://a.ocv.me/pub/g/2025/09/PXL_20250927_160446367~2.jpg), september is an excellent time to start decorating for xmas
<img src="https://a.ocv.me/pub/stuff/padoru.gif" alt="padoru" /> <img src="https://a.ocv.me/pub/stuff/padoru.gif" alt="padoru" /> <img src="https://a.ocv.me/pub/stuff/padoru.gif" alt="padoru" />
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0923-2247 `v1.19.14` Voile, the Magic Library
## 🧪 new features
* #779 add [OPDS](https://opds.io/) support (thx @Scotsguy!) 6dbd9901
* copyparty can now serve books for [KOReader](https://koreader.rocks/)
* [the mandatory soundtrack](https://www.youtube.com/watch?v=F8Aex6tzH-s)
* #786 add Turkish translation (thx @NandeMD!) 549fe33f
* #808 support reading config-files in UTF8-BOM 5e4ff90b
* make more http-errors return a friendly errortext rather than the scary wall of html 9d066414
## 🩹 bugfixes
* #842 could not navpane into webroot if webroot is unmapped 0941fd4e
* upload-resume becomes funky when the OS/network is overloaded to the point where it starts dropping connections left and right -- the issue was reported on discord and I don't have a good way to reproduce it, but these changes may help and/or fix it:
* b136a5b0 panic and drop chunk reservations if client or connection glitches out
* 38df223b also drop reservations if subchunk logic hits an edgecase
## 🔧 other changes
* [versus.md](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) tweaks:
* #840 tooltips in the table headers (thx @guano!) e9ca36fa
* #839 sftpgo updates (thx @augustanational!) a053a663
## 🌠 fun facts
* this release is identical to v1.19.13 except [the pypi package isn't messed up](https://github.com/9001/copyparty/issues/847) 👉😎👉
* as if the 13 wasn't foreshadowing enough
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0921-2211 `v1.19.12` conlangparty
## 🧪 new features
* #787 add Esperanto translation (thx @slashdevslashurandom!) 15d3c2fb
* #802 timezone can be specified for the rotf upload rule (thx @Lehmustus!) 1460fe97
## 🩹 bugfixes
* #837 sharing an entire HDD on Windows ([v1.19.9](https://github.com/9001/copyparty/releases/tag/v1.19.9) regression) 6a244320
* sharing your whole [【Dドライブ】](https://www.youtube.com/watch?v=BFfYrxm2t58) is once again possible
* TLNote: `Dドライブ` means "D:\ drive"
* if you can't upgrade, a workaround is global-option `casechk: n`
* `/?ls` on an unmapped root didn't give a sensible response; now it should be okay except it won't have a `cfg` field 8f6194fe
## 🔧 other changes
* #836 hide the unpost tab in folders where user has no write-access ca872c40
* #835 fix webdep buildscript to avoid an edgecase on some platforms (thx @25huizengek1!) 260da2f4
## 🌠 fun facts
* the esperanto translation was the final straw; `copyparty-sfx.py` is now 1 MiB large
* `copyparty-en.py` is still a comfy 759 KiB
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0920-1011 `v1.19.11` ftp fix
## 🩹 bugfixes
* #827 ftp on servers with unmapped root broke in v1.19.9 280815f1
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0919-2244 `v1.19.10` ramdisk kinshi
## 🧪 new features
* prevent uploading into ramdisks by default 59a01221 538a205c
* safeguard against misconfigured docker containers, where certain parts of the vfs has not been mapped to actual storage, for example `/w/music` is but `/w/` itself isn't
* can be disabled with `wram` (global-option and/or volflag), mainly for ephemeral servers
* #799 nixos: groups can be specified (thx @AnyTimeTraveler!) ee5f3190
* the logspam from the filesystem indexer can be reduced/disabled 478f1c76
* new options `scan-st-r`, `scan-pr-r`, `scan-pr-s`
## 🩹 bugfixes
* #809 medialinks (`#af-badf00d`) would fail on the very first pageload from a new browser 5996a58b
* #806 instructions for running on iOS was bad (thx @GhelloZ!) 35326a6f
## 🔧 other changes
* copyparty32.exe is now english-only, to save space 669b1075
* version info on startup indicates free-threading or not 65591528
* docs: explain the `daw` option better a043d7cf
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0915-0019 `v1.19.9` case-sensitivity, give or take
## 🧪 new features
* #781 case-sensitive behavior is now simulated on Windows/Macos/Fat32/NTFS 8b66874b
* avoids some of the scary issues associated with case-insensitive filesystems
* unfortunately this is expensive and may be **noticeably slower in large folders;** disable the safeguard with `casechk: n` if you know you don't need it
* #789 case-insensitive search for unicode filenames/paths (thx @km-clay!) e2aa8fc1 ecd18adc
* default-disabled because it is somewhat expensive; enable with global-option `srch-icase`
* [CB-1](https://codeberg.org/9001/copyparty/issues/1) add `--qr-stdout` and `--qr-stderr` to show qr-code even with `-q` d7887f3d
## 🩹 bugfixes
* #775 the basic-uploader didn't accept empty files 25749b4b
* opt-out from index.html with `?v` did not work as documented 3d09bec1
* Windows: dedup could get rejected by the filesystem if the origin file had a timestamp from the cambrian era e09f3c9e
* webdav would incorrectly return an error for Depth:0 on an unmapped root 3a2381ff
* markdown-editor would waste another http roundtrip on certain documents 14b7e514
* `--help` didn't render if terminal was non-UTF8 3f454927
## 🔧 other changes
* #788 fixed a hotkey typo in the imageviewer (thx @tkroo!) 5c1a43c7
* #778 improved polish translation (thx @daimond113!) 52438bcc
* #798 debian: fixed an issue in the systemd script (thx @Beethoven-n, and congrats on commit number 4000!) dfd9e007
* media-tag `conductor` is no longer mapped to `circle` (album-artist) 9c9e4057
* "download-selection-as-zip" now produces a better filename, `sel-FOLDERNAME.zip` instead of `FIRSTFILE.zip` 8f587627
* detect and warn if IdP volumes are misconfigured in a particular way 83bd1974
## 🌠 fun facts
* the themesong of this release is [KO3 - Give it up?](https://www.youtube.com/watch?v=8w_na7HAppU) because that's what the car mechanic got to enjoy when i forgot to unplug the flashdrive before handing in the shitbox for service
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0907-2300 `v1.19.8` SECURITY: fix single-file shares
## ⚠️ ATTN: this release fixes [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95), an issue with shares
* when a share is created for just one or more files inside a folder, it was possible to access the other files inside that folder by guessing the filenames
* it was not possible to descend into subdirectories in this manner; only the sibling files were accessible
* NOTE: this does NOT affect filekeys; this is specifically regarding the `shr` global-option
## recent important news
* [v1.19.8 (2025-09-07)](https://github.com/9001/copyparty/releases/tag/v1.19.8) fixed [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) (a missing permission-check inside single-file shares)
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## 🧪 new features
* #761 IdP: option to replace the login/logout links and buttons with redirects into an IdP UI 09f22993
* #726 disk-usage and server-version can be selectively hidden according to user permissions 19a4c453
* option `--shr-who` / volflag `shr_who` decides who is able to create a share of that volume edafa158
* #751 nixos: add globalExtraConfig to specify repeatable config parameters (thx @xvrqt!) 09e3018b
* some very small speedups (mainly u2c and ancient python versions) 74821a38
* #759 #393 total folder size now decreases when files inside are deleted 96b109b0
* would previously require a reindex to get back on track
## 🩹 bugfixes
* fix [GHSA-pxvw-4w88-6x95](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) by fencing fileshares to just the shared files e0a92ba7
* #397 prevent hinting at valid passwords, even if they cannot be used to authenticate with 7a4ee4db
* #747 disable some features if `/tmp` must be used for runtime config e6755aa8
* the config-folder will now also be created with chmod 700 (accessible by owner only)
* #733 #298 fix hotkeys on non-qwerty keyboard layouts (dvorak etc.) e798a9a5
* #539 ftp-server: support clients which never does a CWD b0496311
* ignore the plaintext session-cookie on https; fixes some confusing behavior when switching from https to http c71128fd
* `og-ua` would prevent clients matching the pattern from accessing fullsize files
* `og-ua` was only possible to set globally; the `og_ua` volflag was ignored 422f8f62
* uds / unix-domain-sockets got wrong permissions when `rm-sck` was used e270fe60
* #727 macos: support running from config-files 230a1462
* #539 avoid issues if someone uploads a file with a last-modified timestamp from year -9999999999999 eeb7738b
* using the spacebar to pause a video was jank on chrome bfcb6eac
* block the next-song hotkey while a folder is loading f7e08ed0
* #748 fix rare js-panic when an action is aborted aaeec11f
* #738 bubbleparty: use /bin/bash (thx @ckastner!) 0469b5a2
## 🔧 other changes
* partyfuse: nice speedup by caching `readdir` too 06d2654b
* partyfuse: explain usage with usernames 1cdb3880
* connect-page: better examples when usernames enabled 3bdef75e
* docker: fix image annotations ab562382
## 🌠 fun facts
* konami's biggest legacy lives on f0caf881 bd6d1f96
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0828-2014 `v1.19.7` chdir
## 🧪 new features
* new option `chdir` to change the PWD (process working-directory) before volumes are mapped 14555d58
## 🩹 bugfixes
* fix using empty folders as statefile storage ([v1.19.6](https://github.com/9001/copyparty/releases/tag/v1.19.6) made this a bit too strict) 0d96786e
* holding I/K to scroll through folders quickly now works better 914686ec
## 🔧 other changes
* #717 docker: fix the image repo metadata (thx @EmilyxFox!) 6f087117
* docker: change `$HOME` to `/state` 01cf20a0 d1f75229
* and use the new `chdir` option to preserve old config-file semantics 14555d58
* helps avoid statefiles accidentally landing in `/w` as a consequence of misconfiguration
## 🌠 fun facts
* this release was made at [RevSpace NL](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250828_202820075.jpg?cache)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0827-2038 `v1.19.6` auth-precedence
## 🧪 new features
* #673 add Portuguese translation (thx anonymous!) 4b8c2215
* ...and enable the Polish translation (whoops) 8f235be6
* #689 add option to control authentication priority/precedence 543b7ea9
* url-parameter `?dl` forces file download instead of displaying in-browser 48d6224e
* #533 more ways to make the QR-code always-visible in the console 2848941e
* #695 option to log invalid xml from clients 28b93d79
* #552 configurable markdown newline behavior 0491123b
* and tweak the styling of monospace in links 68503444
## 🩹 bugfixes
* #628 FTP-server now accepts connections from IPv6 link-local addresses 978801d0
* incorrect assumption that all IPv6 link-local addresses start with `fe80` d39c74c1
* ftp: fix file rename d40f061a
* u2c: couldn't upload files located at the very top of the unix file hierarchy 599e82f2
* #699 markdown-editor: fix panic if the table-formatter is executed on something that isn't a table 4c042b3c
## 🔧 other changes
* #696 a volume can be one single file, not just folders aa1c9213
* #442 strongly prefer XDG_CONFIG_HOME as config location 35472557
* #691 album-art collected from audio-files can now become folder thumbnails 0b50fde3
* allow spaces in more of the comma-separated options d30240b4
* docs:
* mention config requirements for [syncing folders](https://github.com/9001/copyparty/#folder-sync) with u2c 6cd0a396 59f142cd
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0821-2319 `v1.19.5` it runs on iOS
## 🧪 new features
* #328 run copyparty on iPhones; see [install on iOS](https://github.com/9001/copyparty#install-on-iOS) in the readme ca98d54f
* cannot run in the background, doesn't have full access to your files, and is slightly buggy, but it *works*
* [running on android](https://github.com/9001/copyparty#install-on-android) gives you a much better experience
* save the qr-code to a file (txt/svg/png) 202ddeac
## 🩹 bugfixes
* #661 fix incorrect `rproxy` hint in the logs 6c76614e
* #649 fix js-crash when tapping in the exactly correct place (thx @hahaslav for debugging!) 0de07d8e
* #628 ftpd: fix banning IPv6 clients 6d76254c
## 🔧 other changes
* #296 nixos: support non-flake setups (thx @Sorixelle!) 20ef74cd 32593670
* config-parser catches and explains a few more common mistakes cc65b1b5
* docs:
* #490, #199: readme: confirm that combining copyparty and syncthing is safe c51371c7
* #377 improved authelia docker example (thx @xFuture603!) cd8771fa
* mention the homebrew formulae f9cb2c15
* #651 versus.md: fix hfs3 comparison (thx @rejetto!) 7a4973fa
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0817-1556 `v1.19.4` take two (fix cfg vols)
## this upgrade is a one-way ticket
* your up2k database (`.hist/up2k.db`), used by the `e2d` filesystem indexing feature, **will be upgraded to a new format** which older copyparty versions cannot read. A backup of each database will be created automatically, named `up2k.db.bak.SOMETHING.v5`. If you need to downgrade to a previous version: Shutdown copyparty, delete these files: `up2k.db up2k.db-shm up2k.db-wal` and then copy `up2k.db.bak.*.v5` to `up2k.db`
## 🧪 new features
* new translations:
* #551 Swedish (thx @Bevinsky!) d676a86f
* #551 Korean (thx @nyqui!) 4e878d2f
* #581 new theme: phi95 (thx @varphi-online!) d8662aeb
* #567 .raw image thumbnails (thx @ar-nelson!) 0177a9b4
* available in docker-images `iv` and `dj`
* #561 epub thumbnails (thx @Scotsguy!) 9435e6b2
* #252 music thumbnails use embdded coverart if available 98d117b8
* thumbnails folder `.hist/th` must be deleted to take effect
* #530 show username of uploaders in file listings; requires `a` (admin) permission 4df033ec
* #604 a new group `@acct` which automatically contains all known usernames 68907eaf
* controlpanel has a dedicated "logout all sessions" button, similar to the logout-link in the browser f4a3fba2
* #397 accounts can be restricted to certian IPs 62e072a2
* #504 automatic login through tailscale auth a4649d1e
* #533 sticky qr-code with `--qr-pin 1` 1ebe06f5
* #572 button to abort copy/move 715d374e
* #618 "download selected files" didn't work on firefox 52 (winxp) dcc6b1b4
* max number of cookies to allow can be configured 6303effe
* good if you have too many selfhosted services on one domain (but will beware of the spec-mandataed max length of the cookie field!)
## 🩹 bugfixes
* fix xvol/xdev edgecases:
* #603 rootless vfs 554cc2f3
* false-positive with overlapping volumes d9046f7e
* #573 ftp: attempting an upload into read-only folder no longer kills the connection 3aa8b7aa
* #306 adjust navpane for `--rp-loc` (location-based proxying)
* #556 more sensible config expansion order f4727f8e
* #624 ...which broke things bf1fdcab
* the video player now stays fullscreen between videos 782e2f1d
* heif thumbnailing with libvips
## 🔧 other changes
* #253 build nix-packages from source (thx @toast003, @chinponya!) 187cae25
* #616 logfiles will have a plaintext severity column if `--no-ansi` d4cf42e7
* #598 separate option `--ac-convt` for audio transcoding timeout d5623057
* #596 users with a blank password gets a strong random-generated one 7f448750
* copyparty.exe: upgrade to python 3.13.7
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0810-1226 `v1.19.1` archlinux fix # 2025-0810-1226 `v1.19.1` archlinux fix

View file

@ -15,7 +15,6 @@
* [general](#general) * [general](#general)
* [event hooks](#event-hooks) - on writing your own [hooks](../README.md#event-hooks) * [event hooks](#event-hooks) - on writing your own [hooks](../README.md#event-hooks)
* [hook effects](#hook-effects) - hooks can cause intentional side-effects * [hook effects](#hook-effects) - hooks can cause intentional side-effects
* [hook import](#hook-import) - the `I` flag runs the hook inside copyparty
* [assumptions](#assumptions) * [assumptions](#assumptions)
* [mdns](#mdns) * [mdns](#mdns)
* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features * [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features
@ -161,12 +160,10 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| method | params | result | | method | params | result |
|--|--|--| |--|--|--|
| GET | `?dl` | download file (don't show in-browser) |
| GET | `?ls` | list files/folders at URL as JSON | | GET | `?ls` | list files/folders at URL as JSON |
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles | | GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
| GET | `?ls=t` | list files/folders at URL as plaintext | | GET | `?ls=t` | list files/folders at URL as plaintext |
| GET | `?ls=v` | list files/folders at URL, terminal-formatted | | GET | `?ls=v` | list files/folders at URL, terminal-formatted |
| GET | `?opds` | list files/folders at URL as opds feed, for e-readers |
| GET | `?lt` | in listings, use symlink timestamps rather than targets | | GET | `?lt` | in listings, use symlink timestamps rather than targets |
| GET | `?b` | list files/folders at URL as simplified HTML | | GET | `?b` | list files/folders at URL as simplified HTML |
| GET | `?tree=.` | list one level of subdirectories inside URL | | GET | `?tree=.` | list one level of subdirectories inside URL |
@ -247,7 +244,6 @@ upload modifiers:
| `Accept: json` | `want=json` | return upload info as json; same as `?j` | | `Accept: json` | `want=json` | return upload info as json; same as `?j` |
| `Rand: 4` | `rand=4` | generate random filename with 4 characters | | `Rand: 4` | `rand=4` | generate random filename with 4 characters |
| `Life: 30` | `life=30` | delete file after 30 seconds | | `Life: 30` | `life=30` | delete file after 30 seconds |
| `Replace: 1` | `replace` | overwrite file if exists |
| `CK: no` | `ck` | disable serverside checksum (maybe faster) | | `CK: no` | `ck` | disable serverside checksum (maybe faster) |
| `CK: md5` | `ck=md5` | return md5 checksum instead of sha512 | | `CK: md5` | `ck=md5` | return md5 checksum instead of sha512 |
| `CK: sha1` | `ck=sha1` | return sha1 checksum | | `CK: sha1` | `ck=sha1` | return sha1 checksum |
@ -256,9 +252,7 @@ upload modifiers:
| `CK: b2s` | `ck=b2s` | return blake2s checksum | | `CK: b2s` | `ck=b2s` | return blake2s checksum |
* `life` only has an effect if the volume has a lifetime, and the volume lifetime must be greater than the file's * `life` only has an effect if the volume has a lifetime, and the volume lifetime must be greater than the file's
* `replace` upload-modifier:
* the header `replace: 1` works for both PUT and multipart-post
* the url-param `replace` only works for multipart-post
* server behavior of `msg` can be reconfigured with `--urlform` * server behavior of `msg` can be reconfigured with `--urlform`
## admin ## admin
@ -311,14 +305,6 @@ a subset of effect types are available for a subset of hook types,
to trigger indexing of files `/foo/1.txt` and `/foo/bar/2.txt`, a hook can `print(json.dumps({"idx":{"vp":["/foo/1.txt","/foo/bar/2.txt"]}}))` (and replace "idx" with "del" to delete instead) to trigger indexing of files `/foo/1.txt` and `/foo/bar/2.txt`, a hook can `print(json.dumps({"idx":{"vp":["/foo/1.txt","/foo/bar/2.txt"]}}))` (and replace "idx" with "del" to delete instead)
* note: paths starting with `/` are absolute URLs, but you can also do `../3.txt` relative to the destination folder of each uploaded file * note: paths starting with `/` are absolute URLs, but you can also do `../3.txt` relative to the destination folder of each uploaded file
## hook import
the `I` flag runs the hook inside copyparty, which can be very useful and dangerous:
* around 140x faster because it doesn't need to launch a new subprocess
* the hook can intentionally (or accidentally) mess with copyparty's internals
* very easy to crash things if not careful
# assumptions # assumptions
@ -342,7 +328,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
the features you can opt to drop are the features you can opt to drop are
* `cm`/easymde, the "fancy" markdown editor, saves ~89k * `cm`/easymde, the "fancy" markdown editor, saves ~89k
* `hl`, prism, the syntax highlighter, saves ~41k * `hl`, prism, the syntax hilighter, saves ~41k
* `fnt`, source-code-pro, the monospace font, saves ~9k * `fnt`, source-code-pro, the monospace font, saves ~9k
for the `re`pack to work, first run one of the sfx'es once to unpack it for the `re`pack to work, first run one of the sfx'es once to unpack it
@ -374,14 +360,6 @@ pip install psutil # better cleanup of stuck metadata parsers on windows
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
``` ```
* on archlinux you can do this:
* `sudo pacman -Sy --needed python-{pip,isort,jinja,argon2-cffi,pyzmq,mutagen,pyftpdlib,pillow}`
* then, as user: `python3 -m pip install --user --break-system-packages -U strip_hints black==21.12b0 click==8.0.2`
* for building docker images: `sudo pacman -Sy --needed qemu-user-static{,-binfmt} podman{,-docker} jq`
* and if you want to run the python 2.7 tests:
* `git clone https://github.com/pyenv/pyenv .pyenv ; cd .pyenv/bin ; env PYTHON_CONFIGURE_OPTS='--enable-optimizations' PYTHON_CFLAGS='-march=native -mtune=native -std=c17' ./pyenv install 2.7.18 -v ; ln -s $HOME/.pyenv/versions/2.7.18/bin/python2 $HOME/bin/`
## just the sfx ## just the sfx

View file

@ -1,6 +1,5 @@
---
services: services:
copyparty: copyparty:
image: copyparty/ac:latest image: copyparty/ac:latest
container_name: copyparty container_name: copyparty

View file

@ -8,7 +8,7 @@ to try this out with minimal adjustments:
* login to https://fs.example.com/ with username `authelia` password `authelia` * login to https://fs.example.com/ with username `authelia` password `authelia`
to use this in a safe and secure manner: to use this in a safe and secure manner:
* follow a guide on setting up [authelia](https://www.authelia.com/integration/proxies/traefik/#docker-compose) properly and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml` * follow a guide on setting up authelia properly (TODO:link) and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
this folder is based on: this folder is based on:
* https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite * https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite
@ -16,18 +16,20 @@ this folder is based on:
incomplete list of modifications made: incomplete list of modifications made:
* support for running with podman as root on fedora (`:z` volumes, `label:disable`) * support for running with podman as root on fedora (`:z` volumes, `label:disable`)
* explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release * explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release
* disabled automatic letsencrypt certificate signing
* reduced logging from debug to info * reduced logging from debug to info
* implemented a docker socket-proxy to not bind the docker.socket directly to traefik * added a warning that traefik is given access to the docker socket (as recommended by traefik docs) which means traefik is able to break out of the container and has full root access on the host machine
* using valkey instead of redis for caching
# security # security
there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62): there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62):
* Move valkey to a private network shared with just authelia * Add in a redis password to limit attacker lateral movement in the system
* Add `watchtower` to manage your image version updates * Move redis to a private network shared with just authelia
* Pin to image hashes (or go all in on updates and add `watchtower`)
* Drop bridge networking for just exposing traefik's public ports * Drop bridge networking for just exposing traefik's public ports
* Configure docker for non-root access to docker socket and then move traefik to use [non-root perms](https://docs.docker.com/engine/security/rootless/)
if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :> if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :>
@ -45,4 +47,4 @@ currently **not optimal,** at least when compared to running the python sfx outs
authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there... authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there...
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't really know where to start I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't relly know where to start

View file

@ -1,14 +1,15 @@
# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/configuration.yml
# Authelia configuration # Authelia configuration
identity_validation: # This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
reset_password: jwt_secret: a_very_important_secret
jwt_secret: 'a_very_important_secret_so_please_change_this'
server: server:
address: 'tcp://:9091' address: 'tcp://:9091'
log: log:
level: info level: info # debug
totp: totp:
issuer: authelia.com issuer: authelia.com
@ -20,26 +21,29 @@ authentication_backend:
access_control: access_control:
default_policy: deny default_policy: deny
rules: rules:
- domain: auth.example.com # Rules applied to everyone
policy: bypass # Allow access to the login UI - domain: traefik.example.com
policy: one_factor
- domain: fs.example.com - domain: fs.example.com
policy: one_factor policy: one_factor
session: session:
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
secret: unsecure_session_secret secret: unsecure_session_secret
cookies: cookies:
- name: authelia_session - name: authelia_session
domain: example.com # this should match whatever your root protected domain is domain: example.com # Should match whatever your root protected domain is
default_redirection_url: https://fs.example.com default_redirection_url: https://fs.example.com
authelia_url: https://authelia.example.com/ authelia_url: https://authelia.example.com/
expiration: 3600 # 1 hour expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes inactivity: 300 # 5 minutes
redis: redis:
host: valkey host: redis
port: 6379 port: 6379
password: your_secure_password_here # This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
# password: authelia
regulation: regulation:
max_retries: 3 max_retries: 3
@ -54,7 +58,9 @@ storage:
notifier: notifier:
disable_startup_check: true disable_startup_check: true
smtp: smtp:
address: 'smtp://127.0.0.1:25' username: test
username: 'test' # This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
password: 'password' password: password
sender: "Authelia <admin@example.com>" host: mail.example.com
port: 25
sender: admin@example.com

View file

@ -1,4 +1,4 @@
--- version: '3.3'
networks: networks:
net: net:
@ -6,7 +6,7 @@ networks:
services: services:
copyparty: copyparty:
image: copyparty/ac:latest image: copyparty/ac
container_name: idp_copyparty container_name: idp_copyparty
user: "1000:1000" # should match the user/group of your fileshare volumes user: "1000:1000" # should match the user/group of your fileshare volumes
volumes: volumes:
@ -19,19 +19,19 @@ services:
labels: labels:
- 'traefik.enable=true' - 'traefik.enable=true'
- 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)' - 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)'
- 'traefik.http.routers.copyparty.entrypoints=websecure' - 'traefik.http.routers.copyparty.entrypoints=https'
- 'traefik.http.routers.copyparty.tls=true' - 'traefik.http.routers.copyparty.tls=true'
- 'traefik.http.routers.copyparty.tls.certresolver=letsencrypt' # ← THIS IS CRUCIAL
- 'traefik.http.routers.copyparty.middlewares=authelia@docker' - 'traefik.http.routers.copyparty.middlewares=authelia@docker'
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
environment: environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit) # ensures log-messages are not delayed (but can reduce speed a tiny bit)
authelia: authelia:
image: authelia/authelia:4.39.5@sha256:023e02e5203dfa0ebaee7a48b5bae34f393d1f9cada4a9df7fbf87eb1759c671 image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax
container_name: idp_authelia container_name: idp_authelia
volumes: volumes:
- ./authelia:/config:z - ./authelia:/config:z
@ -40,23 +40,25 @@ services:
labels: labels:
- 'traefik.enable=true' - 'traefik.enable=true'
- 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)' - 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'
- 'traefik.http.routers.authelia.entrypoints=websecure' - 'traefik.http.routers.authelia.entrypoints=https'
- 'traefik.http.routers.authelia.tls=true' - 'traefik.http.routers.authelia.tls=true'
- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' #- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (1/2)
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com' - 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com'
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true' - 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email' - 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
expose: expose:
- 9091 - 9091
restart: unless-stopped restart: unless-stopped
healthcheck:
disable: true
environment: environment:
- TZ=Etc/UTC - TZ=Etc/UTC
valkey: redis:
image: valkey/valkey:8.1.3-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4 image: redis:7.2.4-alpine3.19
container_name: idp_valkey container_name: idp_redis
volumes: volumes:
- ./valkey:/data:z - ./redis:/data:z
networks: networks:
- net - net
expose: expose:
@ -64,55 +66,40 @@ services:
restart: unless-stopped restart: unless-stopped
environment: environment:
- TZ=Etc/UTC - TZ=Etc/UTC
- VALKEY_EXTRA_FLAGS=--requirepass your_secure_password_here
socket-proxy:
image: lscr.io/linuxserver/socket-proxy:3.2.3@sha256:63d2e0ce6bb0d12dfdbde5c3af31d08fee343ec3801a050c8197a3f5ffae8bed
container_name: idp_socket_proxy
environment:
- CONTAINERS=1
- NETWORKS=1
- EVENTS=1
- PING=1
- VERSION=1
- LOG_LEVEL=warning
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /run
networks:
- net
restart: unless-stopped
expose:
- 2375
traefik: traefik:
image: traefik:3.5.0@sha256:4e7175cfe19be83c6b928cae49dde2f2788fb307189a4dc9550b67acf30c11a5 image: traefik:2.11.0
container_name: idp_traefik container_name: idp_traefik
volumes: volumes:
- ./traefik:/etc/traefik:z - ./traefik:/etc/traefik:z
- /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik
security_opt:
- label:disable # disable selinux because it (rightly) blocks access to docker.sock
networks: networks:
- net - net
labels: labels:
- 'traefik.enable=true' - 'traefik.enable=true'
- 'traefik.http.routers.api.rule=Host(`traefik.example.com`)'
- 'traefik.http.routers.api.entrypoints=https'
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.tls=true'
#- 'traefik.http.routers.api.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (2/2)
- 'traefik.http.routers.api.middlewares=authelia@docker' - 'traefik.http.routers.api.middlewares=authelia@docker'
ports: ports:
- '80:80' - '80:80'
- '443:443' - '443:443'
command: command:
- '--global.sendAnonymousUsage=false' - '--api'
- '--providers.docker.endpoint=tcp://socket-proxy:2375' - '--providers.docker=true'
- '--providers.docker.exposedByDefault=false' - '--providers.docker.exposedByDefault=false'
- '--entrypoints.web.address=:80' - '--entrypoints.http=true'
- '--entrypoints.web.http.redirections.entrypoint.to=websecure' - '--entrypoints.http.address=:80'
- '--entrypoints.web.http.redirections.entrypoint.scheme=https' - '--entrypoints.http.http.redirections.entrypoint.to=https'
- '--entrypoints.websecure.address=:443' - '--entrypoints.http.http.redirections.entrypoint.scheme=https'
- '--entrypoints.https=true'
- '--entrypoints.https.address=:443'
- '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com' - '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com'
- '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json' - '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web' - '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
- '--log.level=INFO' - '--log=true'
depends_on: - '--log.level=WARNING' # DEBUG
- socket-proxy

View file

@ -6,15 +6,15 @@ L: MIT
https://github.com/pallets/jinja/ https://github.com/pallets/jinja/
C: 2007 Pallets C: 2007 Pallets
L: BSD-3-Clause L: BSD 3-Clause
https://github.com/pallets/markupsafe/ https://github.com/pallets/markupsafe/
C: 2010 Pallets C: 2010 Pallets
L: BSD-3-Clause L: BSD 3-Clause
https://github.com/paulc/dnslib/ https://github.com/paulc/dnslib/
C: 2010-2017 Paul Chakravarti C: 2010-2017 Paul Chakravarti
L: BSD-2-Clause L: BSD 2-Clause
https://github.com/pydron/ifaddr/ https://github.com/pydron/ifaddr/
C: 2014 Stefan C. Mueller C: 2014 Stefan C. Mueller
@ -36,10 +36,6 @@ https://github.com/ahupp/python-magic/
C: 2001-2014 Adam Hupp C: 2001-2014 Adam Hupp
L: MIT L: MIT
https://github.com/fusepy/fusepy
C: 2012 Giorgos Verigakis
L: ISC
--- client-side --- software --- --- client-side --- software ---
https://github.com/Daninet/hash-wasm/ https://github.com/Daninet/hash-wasm/
@ -54,10 +50,6 @@ https://github.com/feimosi/baguetteBox.js/
C: 2017 Marek Grzybek C: 2017 Marek Grzybek
L: MIT L: MIT
https://github.com/cure53/DOMPurify/
C: 2025 Cure53 / Mario Heiderich
L: Apache-2.0 or MPL-2.0
https://github.com/markedjs/marked/ https://github.com/markedjs/marked/
C: 2018+, MarkedJS C: 2018+, MarkedJS
C: 2011-2018, Christopher Jeffrey (https://github.com/chjj/) C: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
@ -76,8 +68,8 @@ L: MIT
https://github.com/adobe-fonts/source-code-pro/ https://github.com/adobe-fonts/source-code-pro/
C: 2010-2019 Adobe C: 2010-2019 Adobe
L: OFL-1.1 L: SIL OFL 1.1
https://github.com/FortAwesome/Font-Awesome/ https://github.com/FortAwesome/Font-Awesome/
C: 2022 Fonticons, Inc. C: 2022 Fonticons, Inc.
L: OFL-1.1 L: SIL OFL 1.1

View file

@ -71,7 +71,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}' cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}'
# or if the client you're measuring dies for ~15sec every once ina while and you wanna filter those out, # or if the client youre measuring dies for ~15sec every once ina while and you wanna filter those out,
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}' cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}'
@ -337,5 +337,3 @@ mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfi
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0" mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0" mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
# number of megabytes downloaded since some date
awk </var/log/wjaycore.out '/^..36m2025-05-20/{o=1} !o{next} !/ plain 20[06](,| \[[^,]+\],) +[0-9.]+.\[33m[KM] .* n[0-9]+$/{next} {v=$0;sub(/.* plain 20[06](,| \[[^,]+\],) +/,"",v);sub(/ .*/,"",v);u=v;sub(/.\[.*/,"",v);sub(/.*m/,"",u);$0=u} /[KMG]/{v*=1024} /[MG]/{v*=1024} /G/{v*=1024} {t+=v} END{printf "%d\n",t/(1024*1024)}'

View file

@ -64,8 +64,6 @@ if you give it the value `@ASDF` it will try to open a file named ASDF and send
if the value starts with `%` it will assume a jinja2 template and expand it; the template has access to the `HttpCli` object through a property named `this` as well as everything in `j2a` and the stuff added by `self.j2s`; see [browser.html](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.html) for inspiration or look under the hood in [httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) if the value starts with `%` it will assume a jinja2 template and expand it; the template has access to the `HttpCli` object through a property named `this` as well as everything in `j2a` and the stuff added by `self.j2s`; see [browser.html](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.html) for inspiration or look under the hood in [httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py)
there is also `--html-head-s` and volflag `html_head_s` to add a plain static bit of text, possibly in addition to `--html-head`
# translations # translations

View file

@ -68,7 +68,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
* [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty * [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty
* but anything you want to share must be moved into the kodbox filesystem * but anything you want to share must be moved into the kodbox filesystem
* [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty * [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty
* but their [license (AGPL)](https://snyk.io/learn/agpl-license/) is [thorny](https://opensource.google/documentation/reference/using/agpl-policy) * but their [license](https://snyk.io/learn/agpl-license/) is [problematic](https://opensource.google/documentation/reference/using/agpl-policy)
* and copyparty is way better at uploads in particular (resumable, accelerated) * and copyparty is way better at uploads in particular (resumable, accelerated)
* and anything you want to share must be moved into the respective filesystems * and anything you want to share must be moved into the respective filesystems
* [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui * [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui
@ -85,34 +85,19 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
the table headers in the matrixes below are the different softwares, with a quick review of each software in the next section the table headers in the matrixes below are the different softwares, with a quick review of each software in the next section
the softwares, the softwares,
* `a` = [copyparty](https://github.com/9001/copyparty)
[a]: https://github.com/9001/copyparty "copyparty" * `b` = [hfs2](https://github.com/rejetto/hfs2/) 🔥
[b]: https://github.com/rejetto/hfs2/ "hfs2" * `c` = [hfs3](https://rejetto.com/hfs/)
[c]: https://rejetto.com/hfs/ "hfs3" * `d` = [nextcloud](https://github.com/nextcloud/server)
[d]: https://github.com/nextcloud/server "nextcloud" * `e` = [seafile](https://github.com/haiwen/seafile)
[e]: https://github.com/haiwen/seafile "seafile" * `f` = [rclone](https://github.com/rclone/rclone), specifically `rclone serve webdav .`
[f]: https://github.com/rclone/rclone "rclone" * `g` = [dufs](https://github.com/sigoden/dufs)
[g]: https://github.com/sigoden/dufs "dufs" * `h` = [chibisafe](https://github.com/chibisafe/chibisafe)
[h]: https://github.com/chibisafe/chibisafe "chibisafe" * `i` = [kodbox](https://github.com/kalcaddle/kodbox)
[i]: https://github.com/kalcaddle/kodbox "kodbox" * `j` = [filebrowser](https://github.com/filebrowser/filebrowser)
[j]: https://github.com/filebrowser/filebrowser "filebrowser" * `k` = [filegator](https://github.com/filegator/filegator)
[k]: https://github.com/filegator/filegator "filegator" * `l` = [sftpgo](https://github.com/drakkan/sftpgo)
[l]: https://github.com/drakkan/sftpgo "sftpgo" * `m` = [arozos](https://github.com/tobychui/arozos)
[m]: https://github.com/tobychui/arozos "arozos"
* `a` = [copyparty][a]
* `b` = [hfs2][b] 🔥
* `c` = [hfs3][c]
* `d` = [nextcloud][d]
* `e` = [seafile][e]
* `f` = [rclone][f], specifically `rclone serve webdav .`
* `g` = [dufs][g]
* `h` = [chibisafe][h]
* `i` = [kodbox][i]
* `j` = [filebrowser][j]
* `k` = [filegator][k]
* `l` = [sftpgo][l]
* `m` = [arozos][m]
some softwares not in the matrixes, some softwares not in the matrixes,
* [updog](#updog) * [updog](#updog)
@ -134,18 +119,18 @@ symbol legend,
## general ## general
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| intuitive UX | | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ | | intuitive UX | | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ | | config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ |
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | | | | good documentation | | | | █ | █ | █ | █ | | | █ | █ | | |
| runs on iOS | | | | | | | | | | | | | | | runs on iOS | | | | | | | | | | | | | |
| runs on Android | █ | | █ | | | █ | | | | | | █ | | | runs on Android | █ | | | | | █ | | | | | | | |
| runs on WinXP | █ | █ | | | | █ | | | | | | | | | runs on WinXP | █ | █ | | | | █ | | | | | | | |
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | | █ | █ | █ | █ | | | runs on Windows | █ | █ | █ | █ | █ | █ | █ | | █ | █ | █ | █ | |
| runs on Linux | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | runs on Linux | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | | runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | |
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | | | runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
| runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | | | runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | |
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ | | portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ |
| zero setup, just go | █ | █ | █ | | | | █ | | | █ | | | █ | | zero setup, just go | █ | █ | █ | | | | █ | | | █ | | | █ |
@ -155,15 +140,13 @@ symbol legend,
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever * `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever
* `a`/copyparty remarks: * `a`/copyparty remarks:
* no gui for server settings; only for client-side stuff * no gui for server settings; only for client-side stuff
* runs on iOS / iPads using [a-Shell](https://holzschu.github.io/a-Shell_iOS/) (pretty good) or [iSH](https://ish.app/) (very slow) but cannot run in the background and is not able to share all of your phone storage (just a separate dedicated folder) * can theoretically run on iOS / iPads using [iSH](https://ish.app/), but only the iPad will offer sufficient multitasking i think
* [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only * [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only
* no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading * no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading
* `b`/hfs2 runs on linux through wine * `b`/hfs2 runs on linux through wine
* `f`/rclone must be started with the command `rclone serve webdav .` or similar * `f`/rclone must be started with the command `rclone serve webdav .` or similar
* `h`/chibisafe has undocumented windows support * `h`/chibisafe has undocumented windows support
* `l`/sftpgo: * `i`/sftpgo must be launched with a command
* Must be launched with a command
* On Termux, just run `pkg in sftpgo`
* `m`/arozos has partial windows support * `m`/arozos has partial windows support
@ -171,14 +154,14 @@ symbol legend,
*the thing that copyparty is actually kinda good at* *the thing that copyparty is actually kinda good at*
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| download folder as zip | █ | █ | █ | █ | | | █ | | █ | █ | | █ | | | download folder as zip | █ | █ | █ | █ | | | █ | | █ | █ | | █ | |
| download folder as tar | █ | | | | | | | | | | | | | | download folder as tar | █ | | | | | | | | | | | | |
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ | | upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ |
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ | | parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | | | | resumable uploads | █ | | █ | | | | | | █ | █ | █ | | |
| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | | █ | | upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | | █ |
| upload acceleration | █ | | | | | | | | █ | | █ | | | | upload acceleration | █ | | | | | | | | █ | | █ | | |
| upload verification | █ | | | █ | █ | | | | █ | | | | | | upload verification | █ | | | █ | █ | | | | █ | | | | |
| upload deduplication | █ | | | | █ | | | | █ | | | | | | upload deduplication | █ | | | | █ | | | | █ | | | | |
@ -186,7 +169,7 @@ symbol legend,
| CTRL-V from device | █ | | | █ | | | | | | | | | | | CTRL-V from device | █ | | | █ | | | | | | | | | |
| race the beam ("p2p") | █ | | | | | | | | | | | | | | race the beam ("p2p") | █ | | | | | | | | | | | | |
| "tail -f" streaming | █ | | | | | | | | | | | | | | "tail -f" streaming | █ | | | | | | | | | | | | |
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | | | keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
| upload rules | | | | | | | | | | | | | | | upload rules | | | | | | | | | | | | | |
| ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ | | ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ |
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ | | ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ |
@ -235,7 +218,7 @@ symbol legend,
## protocols and client support ## protocols and client support
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | █ | | serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | █ |
@ -266,9 +249,9 @@ symbol legend,
## server configuration ## server configuration
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| config from cmd args | █ | | | | | █ | █ | | | █ | | | | | config from cmd args | █ | | | | | █ | █ | | | █ | | | |
| config files | █ | █ | █ | | | █ | | █ | | █ | • | | | | config files | █ | █ | █ | | | █ | | █ | | █ | • | | |
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ | | runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ |
| same-port http / https | █ | | | | | | | | | | | | | | same-port http / https | █ | | | | | | | | | | | | |
@ -279,7 +262,7 @@ symbol legend,
* `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead * `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead
* `l`/sftpgo: * `l`/sftpgo:
* config: user can be added by cmd command in [Portable mode](https://docs.sftpgo.com/2.6/cli/#portable-mode); if not in Portable mode users must be added through gui / api calls * config: users must be added through gui / api calls
* `m`/arozos: * `m`/arozos:
* configuration is primarily through GUI * configuration is primarily through GUI
* reverse-proxy is not guaranteed to see the correct client IP * reverse-proxy is not guaranteed to see the correct client IP
@ -287,17 +270,17 @@ symbol legend,
## server capabilities ## server capabilities
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
| per-account chroot | | | | | | | | | | | | █ | | | per-account chroot | | | | | | | | | | | | █ | |
| single-sign-on | | | | █ | █ | | | | • | | | | | | single-sign-on | | | | █ | █ | | | | • | | | | |
| token auth | | | | █ | █ | | | █ | | | | | █ | | token auth | | | | █ | █ | | | █ | | | | | █ |
| 2fa | | | / | █ | █ | | | | | | | █ | | | 2fa | | | | █ | █ | | | | | | | █ | |
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | | █ | █ | | per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | | █ | █ |
| per-folder permissions | | | | █ | █ | | █ | | █ | █ | | █ | █ | | per-folder permissions | | | | █ | █ | | █ | | █ | █ | | █ | █ |
| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ | | per-file permissions | | | | █ | █ | | █ | | █ | | | | █ |
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ | | per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ |
| unmap subfolders | █ | | █ | | | | █ | | | █ | | • | | | unmap subfolders | █ | | █ | | | | █ | | | █ | | • | |
| index.html blocks list | | | | | | | █ | | | • | | | | | index.html blocks list | | | | | | | █ | | | • | | | |
| write-only folders | █ | | █ | | █ | | | | | | █ | █ | | | write-only folders | █ | | █ | | █ | | | | | | █ | █ | |
@ -314,13 +297,13 @@ symbol legend,
| full sync | | | | █ | █ | | | | | | | | | | full sync | | | | █ | █ | | | | | | | | |
| speed throttle | | █ | █ | | | █ | | | █ | | | █ | | | speed throttle | | █ | █ | | | █ | | | █ | | | █ | |
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • | | anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • |
| dyndns updater | | █ | | | | | | | | | | | | | dyndns updater | | █ | | | | | | | | | | | |
| self-updater | | | █ | | | | | | | | | | █ | | self-updater | | | █ | | | | | | | | | | █ |
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • | | log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • |
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | | █ | | upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | | █ |
| prometheus metrics | █ | | | █ | | | | | | | | █ | | | prometheus metrics | █ | | | █ | | | | | | | | █ | |
| curl-friendly ls | █ | | | | | | | | | | | | | | curl-friendly ls | █ | | | | | | | | | | | | |
| curl-friendly upload | █ | | | | | █ | █ | • | | | | | | | curl-friendly upload | █ | | | | | █ | █ | • | | | | | |
* `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path * `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path
* `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that * `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that
@ -349,29 +332,28 @@ symbol legend,
* `upload tracking / log` in main logfile * `upload tracking / log` in main logfile
* `m`/arozos: * `m`/arozos:
* `2fa` maybe possible through LDAP/Oauth * `2fa` maybe possible through LDAP/Oauth
* `c`/hfs3
* `2fa` available by installing a plugin
## client features ## client features
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | | █ | | single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | | █ |
| themes | █ | █ | █ | █ | | | | | █ | | | | | | themes | █ | █ | █ | █ | | | | | █ | | | | |
| directory tree nav | █ | | | | █ | | | | █ | | | | | | directory tree nav | █ | | | | █ | | | | █ | | | | |
| multi-column sorting | █ | | | | | | | | | | | | | | multi-column sorting | █ | | | | | | | | | | | | |
| thumbnails | █ | | / | | | | | █ | █ | | | | █ | | thumbnails | █ | | | | | | | █ | █ | | | | █ |
| ┗ image thumbnails | █ | | / | █ | █ | | | █ | █ | █ | | | █ | | ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | | █ |
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ | | ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ |
| ┗ audio spectrograms | █ | | | | | | | | | | | | | | ┗ audio spectrograms | █ | | | | | | | | | | | | |
| audio player | █ | | | █ | █ | | | | █ | | | | █ | | audio player | █ | | | █ | █ | | | | █ | | | | █ |
| ┗ gapless playback | █ | | | | | | | | • | | | | | | ┗ gapless playback | █ | | | | | | | | • | | | | |
| ┗ audio equalizer | █ | | | | | | | | | | | | | | ┗ audio equalizer | █ | | | | | | | | | | | | |
| ┗ waveform seekbar | █ | | | | | | | | | | | | | | ┗ waveform seekbar | █ | | | | | | | | | | | | |
| ┗ OS integration | █ | | | | | | | | | | | | | | ┗ OS integration | █ | | | | | | | | | | | | |
| ┗ transcode to lossy | █ | | | | | | | | | | | | | | ┗ transcode to lossy | █ | | | | | | | | | | | | |
| video player | █ | | █ | █ | █ | | | | █ | █ | | | █ | | video player | █ | | | █ | █ | | | | █ | █ | | | █ |
| ┗ video transcoding | | | / | | | | | | █ | | | | | | ┗ video transcoding | | | | | | | | | █ | | | | |
| audio BPM detector | █ | | | | | | | | | | | | | | audio BPM detector | █ | | | | | | | | | | | | |
| audio key detector | █ | | | | | | | | | | | | | | audio key detector | █ | | | | | | | | | | | | |
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | | | | | search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | | | |
@ -383,16 +365,16 @@ symbol legend,
| find local file | █ | | | | | | | | | | | | | | find local file | █ | | | | | | | | | | | | |
| undo recent uploads | █ | | | | | | | | | | | | | | undo recent uploads | █ | | | | | | | | | | | | |
| create directories | █ | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ | | create directories | █ | | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | | █ | | image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | | █ |
| markdown viewer | █ | | / | | █ | | | | █ | | | | █ | | markdown viewer | █ | | | | █ | | | | █ | | | | █ |
| markdown editor | █ | | | | █ | | | | █ | | | | █ | | markdown editor | █ | | | | █ | | | | █ | | | | █ |
| readme.md in listing | █ | | / | █ | | | | | | | | | | | readme.md in listing | █ | | | █ | | | | | | | | | |
| rename files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | █ | █ | | rename files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | █ | █ |
| batch rename | █ | | | | | | | | █ | | | | | | batch rename | █ | | | | | | | | █ | | | | |
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ | | cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ |
| move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | | █ | | move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | | █ |
| delete files | █ | █ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ | | delete files | █ | █ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
| copy files | | | / | | █ | | | | █ | █ | █ | █ | █ | | copy files | | | | | █ | | | | █ | █ | █ | | █ |
* `single-page app` = multitasking; possible to continue navigating while uploading * `single-page app` = multitasking; possible to continue navigating while uploading
* `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song * `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song
@ -401,23 +383,22 @@ symbol legend,
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads * `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads
* `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best) * `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)
* `b`/hfs2 has a very basic directory tree view, not showing sibling folders * `b`/hfs2 has a very basic directory tree view, not showing sibling folders
* `c`/hfs3 remarks:
* audio playback does not continue into next song
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav * `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
* `j`/filebrowser remarks: * `j`/filebrowser remarks:
* audio playback does not continue into next song * audio playback does not continue into next song
* plaintext viewer/editor * plaintext viewer/editor
* `k`/filegator directory tree is a modal window * `k`/filegator directory tree is a modal window
* `l`/sftpgo remarks:
* audio/video playback does not continue into next song/video
* plaintext viewer/editor
## integration ## integration
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]| | feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - | | ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
| OS alert on upload | | | | | | | | | | | | | | | OS alert on upload | | | | | | | | | | | | | |
| discord | | | | | | | | | | | | | | | discord | | | | | | | | | | | | | |
| ┗ announce uploads | | | | | | | | | | | | | | | ┗ announce uploads | | | | | | | | | | | | | |
| ┗ custom embeds | | | | | | | | | | | | | | | ┗ custom embeds | | | | | | | | | | | | | |
| sharex | █ | | | █ | | █ | | █ | | | | | | | sharex | █ | | | █ | | █ | | █ | | | | | |
| flameshot | | | | | | █ | | | | | | | | | flameshot | | | | | | █ | | | | | | | |
@ -490,8 +471,10 @@ symbol legend,
* vfs with gui config, per-volume permissions * vfs with gui config, per-volume permissions
* tested locally, v0.53.2 on archlinux * tested locally, v0.53.2 on archlinux
* 🔵 uploads are resumable * 🔵 uploads are resumable
* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare
* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic) * ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)
* ⚠️ uploads are not integrity-checked * ⚠️ uploads are not integrity-checked
* ⚠️ copies the file after upload; need twice filesize free disk space
* ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster) * ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster)
* ⚠️ doesn't support crazy filenames * ⚠️ doesn't support crazy filenames
* ✅ config GUI * ✅ config GUI
@ -592,7 +575,7 @@ symbol legend,
* ✅ file tags; file discussions!? * ✅ file tags; file discussions!?
* ✅ video transcoding * ✅ video transcoding
* ✅ unzip uploaded archives * ✅ unzip uploaded archives
* ✅ IDE with syntax highlighting * ✅ IDE with syntax hilighting
* ✅ wysiwyg editor for openoffice files * ✅ wysiwyg editor for openoffice files
## [filebrowser](https://github.com/filebrowser/filebrowser) ## [filebrowser](https://github.com/filebrowser/filebrowser)
@ -634,8 +617,10 @@ symbol legend,
* ⚠️ across the atlantic, copyparty is 2.5x faster * ⚠️ across the atlantic, copyparty is 2.5x faster
* 🔵 sftp uploads are resumable * 🔵 sftp uploads are resumable
* ⚠️ web UI is very minimal + a bit slow * ⚠️ web UI is very minimal + a bit slow
* ⚠️ no thumbnails * ⚠️ no thumbnails / image viewer / audio player
* ⚠️ basic file manager (no cut/paste/move)
* ⚠️ no filesystem indexing / search * ⚠️ no filesystem indexing / search
* ⚠️ doesn't run on phones, tablets
* ⚠️ no zeroconf (mdns/ssdp) * ⚠️ no zeroconf (mdns/ssdp)
* ⚠️ impractical directory URLs * ⚠️ impractical directory URLs
* ⚠️ AGPL licensed * ⚠️ AGPL licensed

View file

@ -12,7 +12,16 @@
}: }:
{ {
nixosModules.default = ./contrib/nixos/modules/copyparty.nix; nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
overlays.default = import ./contrib/package/nix/overlay.nix; overlays.default = final: prev: {
copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
ffmpeg = final.ffmpeg-full;
};
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./contrib/package/nix/partftpy { };
};
};
};
} }
// flake-utils.lib.eachDefaultSystem ( // flake-utils.lib.eachDefaultSystem (
system: system:

View file

@ -93,7 +93,6 @@ copyparty = [
"web/*.js", "web/*.js",
"web/*.css", "web/*.css",
"web/*.html", "web/*.html",
"web/*.xml",
"web/a/*.bat", "web/a/*.bat",
"web/deps/*.gz", "web/deps/*.gz",
"web/deps/*.woff*", "web/deps/*.woff*",

View file

@ -3,7 +3,7 @@ WORKDIR /z
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \ ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
ver_hashwasm=4.12.0 \ ver_hashwasm=4.12.0 \
ver_marked=4.3.0 \ ver_marked=4.3.0 \
ver_dompf=3.2.7 \ ver_dompf=3.2.6 \
ver_mde=2.18.0 \ ver_mde=2.18.0 \
ver_codemirror=5.65.18 \ ver_codemirror=5.65.18 \
ver_fontawesome=5.13.0 \ ver_fontawesome=5.13.0 \
@ -41,25 +41,25 @@ RUN mkdir -p /z/dist/no-pk \
&& (mkdir hash-wasm \ && (mkdir hash-wasm \
&& cd hash-wasm \ && cd hash-wasm \
&& unzip ../hash-wasm.zip) \ && unzip ../hash-wasm.zip) \
&& (tar --no-same-owner -xf asmcrypto.tgz \ && (tar -xf asmcrypto.tgz \
&& cd asmcrypto.js-$ver_asmcrypto \ && cd asmcrypto.js-$ver_asmcrypto \
&& npm install ) \ && npm install ) \
&& (tar --no-same-owner -xf marked.tgz \ && (tar -xf marked.tgz \
&& cd marked-$ver_marked \ && cd marked-$ver_marked \
&& npm install \ && npm install \
&& npm i grunt uglify-js -g ) \ && npm i grunt uglify-js -g ) \
&& (tar --no-same-owner -xf codemirror.tgz \ && (tar -xf codemirror.tgz \
&& cd codemirror5-$ver_codemirror \ && cd codemirror5-$ver_codemirror \
&& npm install ) \ && npm install ) \
&& (tar --no-same-owner -xf mde.tgz \ && (tar -xf mde.tgz \
&& cd easy-markdown-editor* \ && cd easy-markdown-editor* \
&& npm install \ && npm install \
&& npm i gulp-cli -g ) \ && npm i gulp-cli -g ) \
&& tar --no-same-owner -xf dompurify.tgz \ && tar -xf dompurify.tgz \
&& tar --no-same-owner -xf prism.tgz \ && tar -xf prism.tgz \
&& tar --no-same-owner -xf fusepy.tgz \ && tar -xf fusepy.tgz \
&& unzip fontawesome.zip \ && unzip fontawesome.zip \
&& tar --no-same-owner -xf zopfli.tgz && tar -xf zopfli.tgz
COPY busy-mp3.sh /z/ COPY busy-mp3.sh /z/
@ -68,7 +68,7 @@ RUN /z/busy-mp3.sh \
# build fonttools (which needs zopfli) # build fonttools (which needs zopfli)
RUN tar --no-same-owner -xf zopfli.tgz \ RUN tar -xf zopfli.tgz \
&& cd zopfli* \ && cd zopfli* \
&& cmake \ && cmake \
-DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_INSTALL_PREFIX=/usr \

View file

@ -1,7 +1,7 @@
FROM alpine:latest FROM alpine:latest
WORKDIR /z WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-ac" \ org.opencontainers.image.title="copyparty-ac" \
org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)" org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)"
@ -16,6 +16,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base ADD base ./base
RUN ash innvikler.sh ac RUN ash innvikler.sh ac
WORKDIR /state WORKDIR /w
EXPOSE 3923 EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest FROM alpine:latest
WORKDIR /z WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-dj" \ org.opencontainers.image.title="copyparty-dj" \
org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection" org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection"
@ -38,6 +38,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base ADD base ./base
RUN ash innvikler.sh dj RUN ash innvikler.sh dj
WORKDIR /state WORKDIR /w
EXPOSE 3923 EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest FROM alpine:latest
WORKDIR /z WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-im" \ org.opencontainers.image.title="copyparty-im" \
org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)" org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)"
@ -15,6 +15,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base ADD base ./base
RUN ash innvikler.sh im RUN ash innvikler.sh im
WORKDIR /state WORKDIR /w
EXPOSE 3923 EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest FROM alpine:latest
WORKDIR /z WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-iv" \ org.opencontainers.image.title="copyparty-iv" \
org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)" org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)"
@ -28,6 +28,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base ADD base ./base
RUN ash innvikler.sh iv RUN ash innvikler.sh iv
WORKDIR /state WORKDIR /w
EXPOSE 3923 EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"] ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]

View file

@ -1,7 +1,7 @@
FROM alpine:latest FROM alpine:latest
WORKDIR /z WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty" \ org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-min" \ org.opencontainers.image.title="copyparty-min" \
org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding" org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding"
@ -13,6 +13,6 @@ RUN apk --no-cache add !pyc \
COPY i/dist/copyparty-sfx.py innvikler.sh ./ COPY i/dist/copyparty-sfx.py innvikler.sh ./
RUN ash innvikler.sh min RUN ash innvikler.sh min
WORKDIR /state WORKDIR /w
EXPOSE 3923 EXPOSE 3923
ENTRYPOINT ["python3", "-m", "copyparty", "--no-thumb", "-c", "/z/initcfg"] ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "--no-thumb", "-c", "/z/initcfg"]

View file

@ -1,19 +1,6 @@
#!/bin/ash #!/bin/ash
set -ex set -ex
tmv() {
touch -r "$1" t
mv t "$1"
}
iawk() {
awk "$1" <"$2" >t
tmv "$2"
}
ised() {
sed -r "$1" <"$2" >t
tmv "$2"
}
# use zlib-ng if available # use zlib-ng if available
f=/z/base/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl f=/z/base/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl
[ "$1" != min ] && [ -e $f ] && { [ "$1" != min ] && [ -e $f ] && {
@ -28,15 +15,9 @@ rm -rf /z/base
rm -rf /var/cache/apk/* /root/.cache rm -rf /var/cache/apk/* /root/.cache
# initial config; common for all flavors # initial config; common for all flavors
mkdir /state /cfg /w mkdir /cfg /w
chmod 777 /state /cfg /w chmod 777 /cfg /w
cat >initcfg <<'EOF' echo % /cfg > initcfg
[global]
chdir: /w
no-crt
% /cfg
EOF
# unpack sfx and dive in # unpack sfx and dive in
python3 copyparty-sfx.py --version python3 copyparty-sfx.py --version
@ -51,13 +32,8 @@ rm -rf \
/tmp/pe-* /z/copyparty-sfx.py \ /tmp/pe-* /z/copyparty-sfx.py \
ensurepip pydoc_data turtle.py turtledemo lib2to3 ensurepip pydoc_data turtle.py turtledemo lib2to3
cd /usr/lib/python3.*/site-packages/copyparty/
rm stolen/surrogateescape.py
iawk '/^[^ ]/{s=0}/^if not VENDORED:/{s=1}!s' qrkode.py
iawk '/^[^ ]/{s=0}/^ DNS_VND = False/{s=1;print" raise"}!s' mdns.py
# speedhack # speedhack
ised 's/os.environ.get\("PRTY_NO_IMPRESO"\)/"1"/' util.py sed -ri 's/os.environ.get\("PRTY_NO_IMPRESO"\)/"1"/' /usr/lib/python3.*/site-packages/copyparty/util.py
# drop bytecode # drop bytecode
find / -xdev -name __pycache__ -print0 | xargs -0 rm -rf find / -xdev -name __pycache__ -print0 | xargs -0 rm -rf
@ -70,12 +46,6 @@ find -name __pycache__ |
grep -E 'ty/web/|/pycpar' | grep -E 'ty/web/|/pycpar' |
tr '\n' '\0' | xargs -0 rm -rf tr '\n' '\0' | xargs -0 rm -rf
smoketest() {
# two-for-one: # two-for-one:
# 1) smoketest copyparty even starts # 1) smoketest copyparty even starts
# 2) build any bytecode we missed # 2) build any bytecode we missed
@ -112,95 +82,5 @@ kill $pid; wait $pid
# output from -e2d # output from -e2d
rm -rf .hist /cfg/copyparty rm -rf .hist /cfg/copyparty
}
smoketest
[ "$1" == min ] && {
# shrink amd64 from 45.5 to 33.2 MiB
# libstdc++ is pulled in by libmpdec++ in libmpdec; keep libmpdec.so
cd /usr/lib ; rm -rf \
libmpdec++.so* \
libncurses* \
libpanelw* \
libreadline* \
libstdc++.so* \
--
cd /usr/lib/python3.*/lib-dynload/ ; rm -rf \
*audioop.* \
_asyncio.* \
_ctypes_test.* \
_curses* \
_test* \
_xx* \
ossaudio.* \
readline.* \
xx* \
--
# keep http/client for u2c
cd /usr/lib/python3.*/ ; rm -rf \
site-packages/*.dist-info \
aifc.py \
asyncio \
bdb.py \
cgi.py \
config-3.*/Makefile \
ctypes/macholib \
dbm \
difflib.py \
doctest.py \
email/_header_value_parser.py \
html \
http/cookiejar.* \
http/server.* \
imaplib.py \
importlib/resources \
mailbox.py \
nntplib.py \
pickletools.py \
pydoc.py \
smtplib.py \
statistics.py \
tomllib \
unittest \
urllib/request.* \
venv \
wsgiref \
xml/dom \
xml/sax \
xmlrpc \
--
set +x
find -iname '*.pyc' |
grep -viE 'tftpy' |
while IFS= read -r x; do
y="$(printf '%s\n' "$x" | sed -r 's`/__pycache__/([^/]+)\.cpython-312\.pyc$`/\1.py`')"
[ -e "$y" ] || continue
[ "$y" = "$x" ] && continue
rm "$y"
mv "$x" "${y}c"
done
find -iname __pycache__ -print0 | xargs -0 rm -rf --
rm -rf /a
set -x
smoketest
# printf '%s\n' 'FROM localhost/copyparty-min-amd64' 'COPY a /' 'RUN /bin/ash /a' >Dockerfile
# podman rmi localhost/m2 ; podman build --squash-all -t m2 . && podman images && podman run --rm -it localhost/m2 --exit=idx && podman images
}
# goodbye # goodbye
exec rm innvikler.sh exec rm innvikler.sh

View file

@ -18,13 +18,6 @@ ngs=(
dj-{ppc64le,s390x,arm} dj-{ppc64le,s390x,arm}
) )
err=
for x in awk jq podman python3 tar wget ; do
command -v $x >/dev/null && continue
err=1; echo ERROR: missing dependency: $x
done
[ $err ] && exit 1
for v in "$@"; do for v in "$@"; do
[ "$v" = clean ] && clean=1 [ "$v" = clean ] && clean=1
[ "$v" = hclean ] && hclean=1 [ "$v" = hclean ] && hclean=1
@ -109,18 +102,12 @@ filt=
# arm takes forever so make it top priority # arm takes forever so make it top priority
[ ${a::3} == arm ] && nice= || nice=-n20 [ ${a::3} == arm ] && nice= || nice=-n20
# not sure if this is necessary or if inherit-annotations=false was enough, but won't hurt
readarray -t annot < <(awk <Dockerfile.$i '/org.opencontainers.image/{sub(/[^\.]+/,"");sub(/[" \\]+$/,"");sub(/"/,"");print"--annotation";print"org"$0}')
annot+=( --annotation "org.opencontainers.image.created=$( date -u +%Y-%m-%dT%H:%M:%SZ )" )
# --pull=never does nothing at all btw # --pull=never does nothing at all btw
(set -x (set -x
nice $nice podman build \ nice $nice podman build \
--squash \ --squash \
--pull=never \ --pull=never \
--from localhost/alpine-$a \ --from localhost/alpine-$a \
--inherit-annotations=false \
"${annot[@]}" \
-t copyparty-$i-$a$suf \ -t copyparty-$i-$a$suf \
-f Dockerfile.$i . || -f Dockerfile.$i . ||
(echo $? $i-$a >> err; printf '%096d\n' $(seq 1 42)) (echo $? $i-$a >> err; printf '%096d\n' $(seq 1 42))

View file

@ -16,8 +16,6 @@ lics = [
"MIT License", "MIT License",
"BSD 2-Clause License", "BSD 2-Clause License",
"BSD 3-Clause License", "BSD 3-Clause License",
"ISC License",
"Apache License v2.0",
"SIL Open Font License v1.1", "SIL Open Font License v1.1",
] ]

View file

@ -7,7 +7,7 @@ import subprocess as sp
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43: # to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
_ = r"""" _ = r""""
echo; for a in '' -bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm; do echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43 ./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
""" """
# click [edit] => [select all] # click [edit] => [select all]

Some files were not shown because too many files have changed in this diff Show more