Compare commits

...

48 commits

Author SHA1 Message Date
NecRaul d33d11321f
fix directory sort-order (closes #1119) (#1528);
folders sorted incorrectly due to the trailing slash,
for example `a/` vs. `a b/`
2026-06-16 22:25:08 +00:00
NecRaul e017b1bc6e fix(rotf): use global rotf_tz as volume fallback 2026-06-16 20:40:37 +02:00
ed efa43f891b ffmpeg bwrap sandbox 2026-06-13 21:26:09 +00:00
ed 90639de984 u2c: hash stdin to wark 2026-06-13 11:47:47 +00:00
ed 5dbff4af41 js: mkdir: cd if existed; closes #1512 2026-06-13 11:45:21 +00:00
ed 5beecd6622 fix xbu reloc resume;
both up2k.js and u2c.py would ignore the "partial upload exists"
assuming it was self-inflicted, skipping the file
2026-06-13 11:20:13 +00:00
ed 5a8949d595 vjoins 2026-06-13 11:15:20 +00:00
ed 22c3a3dd46 pop job.vcfg; fixes up2k._snap_reg 2026-06-13 10:39:26 +00:00
ed a00bc93fe1 bsd: signal masking 2026-06-12 23:37:12 +00:00
ed a48afe87b9 docker: alpine 3.24 2026-06-12 21:47:04 +00:00
ed c3eb9ecec2 docs: docker archs 2026-06-12 21:29:35 +00:00
ed 6e75faa623 docker: drop arm32 iv;
deadlock on startup, probably bad cffi
2026-05-26 19:16:31 +00:00
ed 06af60276b update pkgs to 1.20.16 2026-05-26 18:49:07 +00:00
ed cc80ecc341 v1.20.16 2026-05-26 18:45:13 +00:00
ed ce33a88e25 v1.20.15 2026-05-26 18:16:37 +00:00
ed 62dc833273 misc linter 2026-05-26 17:47:52 +00:00
ed f0f6933b4e bpm mtp needs f32le 2026-05-26 17:47:08 +00:00
ed e32718303c xvol: clarify permissions 2026-05-25 20:16:23 +00:00
ed c7b80acd0d fix ?tar on xvol reject;
would panic and kill the connection
instead of just skipping the blocked symlink
2026-05-25 20:06:17 +00:00
ed cc5420a324 download-as-zip: toplevel optional 2026-05-25 18:32:42 +00:00
ed c28aa08b35 ihead filter on ohead too 2026-05-25 16:12:26 +00:00
ed 6183540c61 mde: fix crash on mobile 2026-05-25 15:57:28 +00:00
ed 5e806ec124 PRTY_FFMPEG_BIN 2026-05-25 13:41:24 +00:00
Danila Kamaev 9068ec6a8e
improve opds compatibility (#1463)
use absolute paths and generate IDs
2026-05-25 11:27:01 +00:00
ed 27031f73be clamp lastmod to y6325 (closes #1470);
fromtimestamp fails on year>9999
2026-05-25 11:08:19 +00:00
ed 926c6e814d embedded-cover crop-control; closes #1474 2026-05-25 10:17:03 +00:00
ed 4e9ad781b6 advisory tiers 2026-05-25 10:10:01 +00:00
ed f4f97b6cc3 make signal-handler less shit;
previously: threading.Condition to wakeup the actual handler;
exciting chance of heisenbugs / deadlocks (theoretically)

almost went with os.pipe on unix and socketpairs on windows,
but turns out SimpleQueue is perfect and safe for this purpose

SimpleQueue is 3.7+ so use a regular queue on <3.7
(same problems as original approach)

also need dedicated thread for popping the queue on <3.7
to avoid deadlock on most platforms (--sig-thr)

new features:

--logrot-sig sets a signal for immediate log-rotate

--stack-sig sets a signal to dump stack to log/stdout

--reload-sig sets a signal to initiates config-reload
   (was hardcoded to USR1 previously)
2026-05-25 00:37:52 +00:00
ed f23ec5d9f8 tests: 0bb80e92 2026-05-23 20:56:44 +00:00
ed 348b4bb5c7 drop rawpy, use libraw/dcraw_emu directly;
rawpy is still supported but will not be bundled by default
due to security concerns

dcraw_emu reads more formats than rawpy + gives better quality
(we told rawpy to use embedded thumbs), so also much slower

dcraw_emu must be combined with libvips or pillow (equivalent)

other alternatives considered:

libvips + a full imagemagick does a different subset of formats,
less than dcraw_emu, yet is 3x slower and eats ram

magick wins wrt formats but is even slower (4x of dcraw_emu)
2026-05-23 20:55:33 +00:00
ed 2c778e0828 add font orbitron for ui-v1.5 2026-05-23 17:54:10 +00:00
exci 7d81b9e837
fix dsel error with bbox active (#1494)
stop error when dragging outside window with dsel active; fixes 1491
2026-05-23 14:15:00 +00:00
ed ca406472f4 readme: S6_NOTIFY_FD 2026-05-22 21:35:59 +00:00
ed 30b23c6ae8 black 2026-05-22 17:42:41 +00:00
ed 9b0268970c sd_notify only when set 2026-05-22 17:42:04 +00:00
Mobin 8c201b844e
s6-notify: support fd-based s6 notification protocol (#1466)
Dinit and s6 supervision suite are known to support this protocol.

See https://skarnet.org/software/s6/notifywhenup.html and
https://davmac.org/projects/dinit/man-pages-html/dinit-service.5.html#ready

Signed-off-by: Mobin Aydinfar <mobin@mobintestserver.ir>
2026-05-22 17:26:47 +00:00
ed b2401ff15a partyfuse: prefer mfusepy (fuse.py fork);
now supports both fuse2 and fuse3

fallback on fuse.py (fuse2-only) if mfusepy unavailable

fuse3 is 20% faster on large files,
fuse2 == fuse3 on small files

motivated by nixos dropping fuse2 in NixOS/nixpkgs#522340
2026-05-21 23:31:57 +00:00
ed 1f4246c6bb support .mka; closes #1489 2026-05-21 00:27:41 +00:00
Lydia Vierkorn 83dc20f33e
spectrograms: option to use log frequency scale (#1487) 2026-05-20 22:34:48 +00:00
Princess Grace f432ef6d51
freebsd: fix deps in rc.d (#1479)
delay startup until filesystems are in place

Signed-off-by: Princess Grace <10965841+Kansattica@users.noreply.github.com>
2026-05-20 22:32:55 +00:00
ilotoki0804 d7eb556cdd
docs: macOS low-ports + sfx building (#1458)
Signed-off-by: ilotoki0804 <ilotoki0804@gmail.com>
2026-05-20 22:30:37 +00:00
AppleTheGolden 0bb80e9294
auth: read (and admin) implies get (#1485)
and hides redundant get-permission in webui; closes #1483
2026-05-20 22:27:25 +00:00
AppleTheGolden 3b53a228b0
invalidate get-only shares when creator no longer exists (#1482)
Fixes #1480
2026-05-15 19:51:12 +00:00
ed 139ef1851e docker-build: cache ffmpeg if stock 2026-05-08 00:02:54 +00:00
ed 2a4c82c742 fix docker makefile; broke from bitrot since nobody's using it
(the podman approach (make.sh) is recommended)
2026-05-07 18:15:59 +00:00
ed b009c585a6 rawpy-0.27 broke installing from github-tarball 2026-05-07 17:34:29 +00:00
ed da6e2ddca9 pyinstaller/win10exe: fix virtio/spice iso sha512;
forgot to update checksum when bumping version,
verified against internetarchive and old local copy
2026-05-02 17:52:33 +00:00
ed 6e25d648a9 update pkgs to 1.20.14 2026-04-24 22:27:49 +00:00
53 changed files with 871 additions and 363 deletions

1
.gitignore vendored
View file

@ -35,6 +35,7 @@ scripts/docker/base/test-aac/
scripts/docker/base/whl/ scripts/docker/base/whl/
scripts/docker/i/ scripts/docker/i/
scripts/deps-docker/uncomment.py scripts/deps-docker/uncomment.py
scripts/deps-docker/unhint.py
contrib/package/arch/pkg/ contrib/package/arch/pkg/
contrib/package/arch/src/ contrib/package/arch/src/

View file

@ -242,6 +242,11 @@ you may also want these, especially on servers:
* [nixos module](#nixos-module) to run copyparty on NixOS hosts * [nixos module](#nixos-module) to run copyparty on NixOS hosts
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https) * [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https)
because the following environment variables are commonly used in service-scripts, they are understood by copyparty:
* `NOTIFY_SOCKET` as provided by systemd with service type=notify (see systemd/copyparty.service above)
* `S6_NOTIFY_FD` for s6/dinit [`ready-notification = pipevar:S6_NOTIFY_FD`](https://skarnet.org/software/s6/notifywhenup.html)
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer: and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
``` ```
firewall-cmd --permanent --add-port={80,443,3921,3922,3923,3945,3990}/tcp # --zone=libvirt firewall-cmd --permanent --add-port={80,443,3921,3922,3923,3945,3990}/tcp # --zone=libvirt
@ -297,7 +302,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ 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
* ☑ ...of RAW images using rawpy * ☑ ...of RAW images using libraw-dcraw_emu or rawpy
* ☑ ...of videos using FFmpeg * ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ cache eviction (max-age; maybe max-size eventually)
@ -831,6 +836,7 @@ you can also zip a selection of files or folders by clicking them in the browser
cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive
* super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways * super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways
* and url-param `&name=foo` changes the name of the toplevel folder in the archive to `foo`, and just `&name` removes the folder entirely
* and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them * and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms) * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms)
* can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0` * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
@ -1181,7 +1187,7 @@ open the `[🎺]` media-player-settings tab to configure it,
* `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3 * `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3
* `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3 * `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3
* `[oth]` converts all other known formats into opus (if supported by browser) or mp3 * `[oth]` converts all other known formats into opus (if supported by browser) or mp3
* `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk` * `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mka|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk`
* "transcode to": * "transcode to":
* `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs) * `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs)
* `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1 * `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1
@ -1331,9 +1337,15 @@ using arguments or config files, or a mix of both:
sleep better at night by telling copyparty to periodically check whether your version has a [known vulnerability](https://github.com/9001/copyparty/security/advisories) sleep better at night by telling copyparty to periodically check whether your version has a [known vulnerability](https://github.com/9001/copyparty/security/advisories)
this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; all of them provide the same information, so which one you choose is whatever this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; choose what severity level you want to be notified for:
* `https://api.copyparty.eu/advisories` * `https://api.copyparty.eu/advisories-panic` -- only really bad stuff, the "UPGRADE NOW" kind
* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9` * `https://api.copyparty.eu/advisories` -- everything important / noteworthy, "upgrade when you can"
* `https://api.copyparty.eu/advisories-all` -- *everything*, including stuff that's unlikely to affect anyone
* `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9` -- same as `advisories-all`
note that `https://api.copyparty.eu/advisories` may (for example) skip some advisories rated `High` but include some `Low`; that's because an easily-reachable `Low` in a default-enabled feature is more severe than a `High` which is a theoretical bug in a contrived use of a fringe feature, but the CVE calculator would still classify that as `High`
if you want to use the github advisory feed but only care about advisories rated `medium`/`moderate` or higher, then global-option `--vc-sev medium` does that, but see previous paragraph
> to see what happens when a bad version is detected, try `--vc-url https://api.copyparty.eu/advisories-test` > to see what happens when a bad version is detected, try `--vc-url https://api.copyparty.eu/advisories-test`
@ -1349,6 +1361,7 @@ config file example:
vc-url: https://api.copyparty.eu/advisories vc-url: https://api.copyparty.eu/advisories
vc-age: 3 # how many hours to wait between each check vc-age: 3 # how many hours to wait between each check
vc-exit # emergency-exit if current version is vulnerable vc-exit # emergency-exit if current version is vulnerable
vc-sev: medium # only care about severity 'Medium'/'Moderate' or higher (github-only; don't use this with api.copyparty.eu)
``` ```
@ -1757,7 +1770,7 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
* symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access * symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access, keeping permissions from outer/initial volume
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
@ -2321,9 +2334,7 @@ if you want to change the fonts, see [./docs/rice/](./docs/rice/)
become a *real* webserver which people can access by just going to your IP or domain without specifying a port become a *real* webserver which people can access by just going to your IP or domain without specifying a port
**if you're on windows,** then you just need to add the commandline argument `-p 80,443` and you're done! nice **if you're on windows or macos,** then you just need to add the commandline argument `-p 80,443` and you're done! nice
**if you're on macos,** sorry, I don't know
**if you're on Linux,** you have the following 4 options: **if you're on Linux,** you have the following 4 options:
@ -3203,7 +3214,7 @@ enable [thumbnails](#thumbnails) of...
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif` * **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+ * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
* **JPEG XL pictures:** `pyvips` or `ffmpeg` * **JPEG XL pictures:** `pyvips` or `ffmpeg`
* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats) * **RAW photos:** either `libraw dcraw_emu` or `rawpy`, plus either `pyvips` or `Pillow`
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq` enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
@ -3215,6 +3226,8 @@ enable [smb](#smb-server) support (**not** recommended): `impacket==0.13.0`
to install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\Windows\System32` or any other folder that's in your `%PATH%` to install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\Windows\System32` or any other folder that's in your `%PATH%`
if your ffmpeg/ffprobe binaries have nonstandard names -- such as `ffmpeg8` (macports) -- set environment variables `PRTY_FFMPEG_BIN` and `PRTY_FFPROBE_BIN` to the corret name (or full path)
### dependency chickenbits ### dependency chickenbits
@ -3229,6 +3242,7 @@ set any of the following environment variables to disable its associated optiona
| -------------------- | ------------ | | -------------------- | ------------ |
| `PRTY_NO_ARGON2` | disable argon2-cffi password hashing | | `PRTY_NO_ARGON2` | disable argon2-cffi password hashing |
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) | | `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
| `PRTY_NO_DCRAW` | disable all [libraw](https://www.libraw.org/homepage)-based thumbnail support for RAW images |
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips | | `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen | | `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
| `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection | | `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |
@ -3243,7 +3257,8 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow | | `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows | | `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
| `PRTY_NO_PYFTPD` | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) | | `PRTY_NO_PYFTPD` | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) |
| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images | | `PRTY_NO_RAW` | same as `PRTY_NO_DCRAW` plus `PRTY_NO_RAWPY` |
| `PRTY_NO_RAWPY` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |
| `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg | | `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg |
example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py` example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`

View file

@ -26,12 +26,12 @@ and consider using [../docs/rclone.md](../docs/rclone.md) instead; usually a bit
## to run this on windows: ## to run this on windows:
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/) * install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
* [x] add python 3.x to PATH (it asks during install) * [x] add python 3.x to PATH (it asks during install)
* `python -m pip install --user fusepy` (or grab a copy of `fuse.py` from the `connect` page on your copyparty, and keep it in the same folder) * `python -m pip install --user mfusepy` (or grab a copy of `mfusepy.py` from the `connect` page on your copyparty, and keep it in the same folder)
* `python ./partyfuse.py n: http://192.168.1.69:3923/` * `python ./partyfuse.py n: http://192.168.1.69:3923/`
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled: 10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
* `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}` * `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}`
* `/mingw64/bin/python3 -m pip install --user fusepy` * `/mingw64/bin/python3 -m pip install --user mfusepy`
* `/mingw64/bin/python3 ./partyfuse.py [...]` * `/mingw64/bin/python3 ./partyfuse.py [...]`
you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE) you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)

View file

@ -21,7 +21,7 @@ usage:
python partyfuse.py http://192.168.1.69:3923/ ./music python partyfuse.py http://192.168.1.69:3923/ ./music
dependencies: dependencies:
python3 -m pip install --user fusepy # or grab it from the connect page python3 -m pip install --user mfusepy # or grab it from the connect page
+ on Linux: sudo apk add fuse + on Linux: sudo apk add fuse
+ on Macos: https://osxfuse.github.io/ + on Macos: https://osxfuse.github.io/
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest + on Windows: https://github.com/billziss-gh/winfsp/releases/latest
@ -91,9 +91,14 @@ info = dbg = nullfun
is_dbg = False is_dbg = False
try:
from mfusepy import FUSE, FuseOSError, Operations
except:
try: try:
from fuse import FUSE, FuseOSError, Operations from fuse import FUSE, FuseOSError, Operations
except: except:
FUSE = None
if WINDOWS: if WINDOWS:
libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest" libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest"
elif MACOS: elif MACOS:
@ -102,10 +107,11 @@ except:
libfuse = "apt install libfuse2\n modprobe fuse" libfuse = "apt install libfuse2\n modprobe fuse"
m = """\033[33m m = """\033[33m
could not import fuse; these may help: could not import mfusepy; these may help:
{} -m pip install --user fusepy {} -m pip install --user mfusepy
{} {}
\033[0m""" \033[0m"""
if not FUSE:
print(m.format(sys.executable, libfuse)) print(m.format(sys.executable, libfuse))
raise raise
@ -143,10 +149,10 @@ def fancy_log(fmt, *a):
def register_wtf8(): def register_wtf8():
def wtf8_enc(text): def wtf8_enc(text, errors=""):
return str(text).encode("utf-8", "surrogateescape"), len(text) return str(text).encode("utf-8", "surrogateescape"), len(text)
def wtf8_dec(binary): def wtf8_dec(binary, errors=""):
return bytes(binary).decode("utf-8", "surrogateescape"), len(binary) return bytes(binary).decode("utf-8", "surrogateescape"), len(binary)
def wtf8_search(encoding_name): def wtf8_search(encoding_name):

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.20" S_VERSION = "2.22"
S_BUILD_DT = "2026-04-22" S_BUILD_DT = "2026-06-13"
""" """
u2c.py: upload to copyparty u2c.py: upload to copyparty
@ -733,16 +733,22 @@ def up2k_chunksize(filesize):
stepsize *= mul stepsize *= mul
# mostly from copyparty/up2k.py
def get_hashlist(file, pcb, mth): def get_hashlist(file, pcb, mth):
# type: (File, Any, Any) -> None # type: (File, Any, Any) -> None
with open(file.abs, "rb", 512 * 1024) as f:
get_hashlist_f(file, pcb, mth, f)
# mostly from copyparty/up2k.py
def get_hashlist_f(file, pcb, mth, f):
# type: (File, Any, Any, Any) -> None
"""generates the up2k hashlist from file contents, inserts it into `file`""" """generates the up2k hashlist from file contents, inserts it into `file`"""
chunk_sz = up2k_chunksize(file.size) chunk_sz = up2k_chunksize(file.size)
file_rem = file.size file_rem = file.size
file_ofs = 0 file_ofs = 0
ret = [] ret = []
with open(file.abs, "rb", 512 * 1024) as f: if True:
t0 = time.time() t0 = time.time()
if mth and file.size >= 1024 * 512: if mth and file.size >= 1024 * 512:
@ -778,6 +784,18 @@ def get_hashlist(file, pcb, mth):
file.kchunks[k] = [v1, v2] file.kchunks[k] = [v1, v2]
def wark_stdin(ar):
file = File(b"", b"", int(ar.files[0]), 0)
get_hashlist_f(file, None, False, sys.stdin if PY2 else sys.stdin.buffer)
if ar.chs:
zsl = ["%s %d %d" % (zsii[0], n, zsii[1]) for n, zsii in enumerate(file.cids)]
print("chs:\n" + "\n".join(zsl))
zsl = [ar.wsalt, str(file.size)] + [x[0] for x in file.cids]
zb = hashlib.sha512("\n".join(zsl).encode("utf-8")).digest()[:33]
wark = ub64enc(zb).decode("utf-8")
print(wark)
def printlink(ar, purl, name, fk): def printlink(ar, purl, name, fk):
if not name: if not name:
url = purl # srch url = purl # srch
@ -1311,7 +1329,8 @@ class Ctl(object):
if self.ar.jw: if self.ar.jw:
print("%s %s" % (wark, vp)) print("%s %s" % (wark, vp))
else: else:
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC) tsdt = datetime.datetime.fromtimestamp
zd = tsdt(max(0, min(2 << 36, file.lmod)), UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year, zd.year,
zd.month, zd.month,
@ -1586,6 +1605,7 @@ NOTE: if server has --usernames enabled, then password is "username:password"
ap.add_argument("--wsalt", type=unicode, metavar="S", default="hunter2", help="salt to use when creating warks; must match server config") ap.add_argument("--wsalt", type=unicode, metavar="S", default="hunter2", help="salt to use when creating warks; must match server config")
ap.add_argument("--chs", action="store_true", help="verbose (print the hash/offset of each chunk in each file)") ap.add_argument("--chs", action="store_true", help="verbose (print the hash/offset of each chunk in each file)")
ap.add_argument("--jw", action="store_true", help="just identifier+filepath, not mtime/size too") ap.add_argument("--jw", action="store_true", help="just identifier+filepath, not mtime/size too")
ap.add_argument("--stdin", action="store_true", help="calculate file-ID of stdin; u2c.py --stdin - $LEN < a.mkv")
ap = app.add_argument_group("performance tweaks") ap = app.add_argument_group("performance tweaks")
ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections") ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections")
@ -1616,6 +1636,10 @@ NOTE: if server has --usernames enabled, then password is "username:password"
except: except:
pass pass
if ar.stdin:
wark_stdin(ar)
return
# msys2 doesn't uncygpath absolute paths with whitespace # msys2 doesn't uncygpath absolute paths with whitespace
if not VT100: if not VT100:
zsl = [] zsl = []

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.20.13" pkgver="1.20.16"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@ -24,7 +24,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=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271") sha256sums=("625f95d65d95cdd6898510518d013905e6766c7d2ae0ea9ae7d5dec96e89e02d")
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.20.13 pkgver=1.20.16
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") 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=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271") sha256sums=("625f95d65d95cdd6898510518d013905e6766c7d2ae0ea9ae7d5dec96e89e02d")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.20.13/copyparty-1.20.13.tar.gz", "url": "https://github.com/9001/copyparty/releases/download/v1.20.16/copyparty-1.20.16.tar.gz",
"version": "1.20.13", "version": "1.20.16",
"hash": "sha256-sq+SUPfvl6XfJt9BLuCCxtK+DwzTHVebT7tqovPlwnE=" "hash": "sha256-Yl+V1l2VzdaJhRBRjQE5BeZ2bH0q4Oqa59XeyW6J4C0="
} }

View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# PROVIDE: copyparty # PROVIDE: copyparty
# REQUIRE: networking # REQUIRE: networking DAEMON FILESYSTEMS mountd
# KEYWORD: # KEYWORD:
. /etc/rc.subr . /etc/rc.subr

View file

@ -77,7 +77,7 @@ web/deps/busy.mp3
web/deps/easymde.css web/deps/easymde.css
web/deps/easymde.js web/deps/easymde.js
web/deps/marked.js web/deps/marked.js
web/deps/fuse.py web/deps/mfusepy.py
web/deps/mini-fa.css web/deps/mini-fa.css
web/deps/mini-fa.woff web/deps/mini-fa.woff
web/deps/prism.css web/deps/prism.css

View file

@ -28,6 +28,7 @@ from .__init__ import (
MACOS, MACOS,
PY2, PY2,
PY36, PY36,
UNIX,
VT100, VT100,
WINDOWS, WINDOWS,
E, E,
@ -45,6 +46,7 @@ from .util import (
DEF_EXP, DEF_EXP,
DEF_MTE, DEF_MTE,
DEF_MTH, DEF_MTH,
HAVE_BWRAP,
HAVE_IPV6, HAVE_IPV6,
IMPLICATIONS, IMPLICATIONS,
JINJA_VER, JINJA_VER,
@ -1234,8 +1236,10 @@ def add_general(ap, nc, srvname):
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)")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best")
ap2.add_argument("--reload-sig", metavar="S", type=u, default=("" if ANYWIN else "USR1"), help="reload server config when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR1\033[0m], [\033[32mUSR1\033[0m], [\033[32m10\033[0m]")
ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)") ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)")
ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks") ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks")
ap2.add_argument("--vc-sev", metavar="LEVEL", type=u, default="low", help="minimum severity to care about; one of these: \033[32mlow medium high critical\033[0m")
ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable") ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable")
ap2.add_argument("--license", action="store_true", help="show licenses and exit") ap2.add_argument("--license", action="store_true", help="show licenses and exit")
ap2.add_argument("--version", action="store_true", help="show versions and exit") ap2.add_argument("--version", action="store_true", help="show versions and exit")
@ -1641,12 +1645,35 @@ def add_optouts(ap):
def add_safety(ap): def add_safety(ap):
th_bwrap = ""
if HAVE_BWRAP:
zsl = [
"bwrap",
"--proc /proc",
"--tmpfs /tmp",
"--tmpfs /var",
"--tmpfs /run",
"--dev-bind /dev/null /dev/null",
"--dev-bind /dev/random /dev/random",
"--dev-bind /dev/urandom /dev/urandom",
"--chdir /tmp",
"--clearenv",
"--unshare-all",
"--cap-drop ALL",
"--die-with-parent",
"--new-session",
]
for d in ("/lib", "/lib64", "/usr/lib", "/usr/lib64"):
if os.path.isdir(d):
zsl.append(" --ro-bind %s %s" % (d, d))
th_bwrap = " ".join(zsl)
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 --no-html --no-readme --no-logues --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 --no-html --no-readme --no-logues --unpost=0 --no-del --no-mv --reflink --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 also has access (keeps permissions from the outer/initial volume) (volflag=xvol)")
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)") ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)") ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)")
ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)") ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)")
@ -1677,6 +1704,8 @@ def add_safety(ap):
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]") ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives") ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)") ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
if not ANYWIN and not UNIX:
ap2.add_argument("--th-bwrap", metavar="CMD", type=u, default=th_bwrap, help="optional bwrap sandbox command for FFmpeg and dcraw (Linux-only)")
def add_salt(ap, fk_salt, dk_salt, ah_salt): def add_salt(ap, fk_salt, dk_salt, ah_salt):
@ -1706,6 +1735,7 @@ def add_logging(ap):
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color") ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color")
ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m") ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m")
ap2.add_argument("--logrot-sig", metavar="S", type=u, default="", help="immediately logrotate when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGHUP\033[0m], [\033[32mHUP\033[0m], [\033[32m1\033[0m]")
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
@ -1769,16 +1799,17 @@ def add_thumbnail(ap):
ap2.add_argument("--th-pre-rl", metavar="SEC", type=int, default=30, help="while pregen is running, ratelimit the thumbnailer logger to one message every \033[33mSEC\033[0m seconds (only works with \033[33m-j1\033[0m); set 0 to disable ratelimit") ap2.add_argument("--th-pre-rl", metavar="SEC", type=int, default=30, help="while pregen is running, ratelimit the thumbnailer logger to one message every \033[33mSEC\033[0m seconds (only works with \033[33m-j1\033[0m); set 0 to disable ratelimit")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec") ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
ap2.add_argument("--th-spec-fl", action="store_true", help="generate spectrograms with logarithmic frequency scale instead of linear")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips # https://github.com/libvips/libvips
# https://stackoverflow.com/a/47612661 # https://stackoverflow.com/a/47612661
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,mdc,mef,mrw,nef,nii,pfm,pgm,png,ppm,raf,raw,sr2,srf,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,arw,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,kdc,mdc,mef,mrw,nef,nii,nrw,orf,pfm,pgm,png,ppm,raf,raw,rw2,sr2,srf,srw,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,rw2,sr2,srf,srw,x3f", help="image formats to decode using rawpy (if available) or libraw's dcraw_emu")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)") ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg") ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
@ -1979,10 +2010,15 @@ def add_debug(ap):
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead") ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests") ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
if ANYWIN or sys.version_info < (3, 7):
ap2.add_argument("--sig-thr", action="store_true", default=True, help=argparse.SUPPRESS)
else:
ap2.add_argument("--sig-thr", action="store_true", help="start separate thread for OS-signals (try this if CTRL-C is busted)")
ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind") ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind")
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks") ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc") ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60") ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
ap2.add_argument("--stack-sig", metavar="S", type=u, default="", help="show stacktrace when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR2\033[0m], [\033[32mUSR2\033[0m], [\033[32m12\033[0m]")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m") ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m")
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files") ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files")
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m") ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")

View file

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 20, 14) VERSION = (1, 20, 16)
CODENAME = "sftp is fine too" CODENAME = "sftp is fine too"
BUILD_DT = (2026, 4, 24) BUILD_DT = (2026, 5, 26)
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

@ -627,6 +627,9 @@ class VFS(object):
t = "%s has no %s in %r => %r => %r" t = "%s has no %s in %r => %r => %r"
self.log("vfs", t % (uname, msg, vpath, cvpath, ap), 6) self.log("vfs", t % (uname, msg, vpath, cvpath, ap), 6)
if not err:
return None
t = "you don't have %s-access in %r or below %r" t = "you don't have %s-access in %r or below %r"
raise Pebkac(err, t % (msg, "/" + cvpath, "/" + vn.vpath)) raise Pebkac(err, t % (msg, "/" + cvpath, "/" + vn.vpath))
@ -858,7 +861,7 @@ class VFS(object):
for le in vfs_ls: for le in vfs_ls:
ap = absreal(os.path.join(fsroot, le[0])) ap = absreal(os.path.join(fsroot, le[0]))
vn2 = self.chk_ap(ap) vn2 = self.chk_ap(ap)
if not vn2 or not vn2.get("", uname, True, False): if not vn2 or not vn2.get("", uname, True, False, err=0):
rm1.append(le) rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore _ = [vfs_ls.remove(x) for x in rm1] # type: ignore
@ -900,20 +903,14 @@ class VFS(object):
def zipgen( def zipgen(
self, self,
vpath: str, folder: str,
vrem: str, vrem: str,
flt: set[str], flt: set[str],
uname: str, uname: str,
dirs: bool, dirs: bool,
dots: int, dots: int,
scandir: bool, scandir: bool,
wrap: bool = True,
) -> Generator[dict[str, Any], None, None]: ) -> Generator[dict[str, Any], None, None]:
# if multiselect: add all items to archive root
# if single folder: the folder itself is the top-level item
folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top")
g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False) g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False)
for _, _, vpath, apath, files, rd, vd in g: for _, _, vpath, apath, files, rd, vd in g:
if flt: if flt:
@ -1663,7 +1660,8 @@ class AuthSrv(object):
for alias, mapping in [ for alias, mapping in [
("h", "gh"), ("h", "gh"),
("G", "gG"), ("G", "gG"),
("A", "rwmda.A"), ("r", "g"),
("A", "rgwmda.A"),
]: ]:
expanded = "" expanded = ""
for ch in mapping: for ch in mapping:
@ -2330,7 +2328,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, vol.flags.get("rotf_tz", self.args.rotf_tz) or "UTC")
zs = vol.flags.get("maxn") zs = vol.flags.get("maxn")
if zs: if zs:
@ -3081,10 +3079,10 @@ class AuthSrv(object):
pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x]) pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x])
if pwds: if pwds:
if self.ah.on: if self.ah.on:
zs = r"(\[H\] %s:.*|[?&]%s=)([^&]+)" zs = r"(\[[HO]\] %s:.*|[?&]%s=)([^&]+)"
zs = zs % (self.args.pw_hdr, self.args.pw_urlp) zs = zs % (self.args.pw_hdr, self.args.pw_urlp)
else: else:
zs = r"(\[H\] %s:.*|=)(" % (self.args.pw_hdr,) zs = r"(\[[HO]\] %s:.*|=)(" % (self.args.pw_hdr,)
zs += "|".join(pwds) + r")([]&; ]|$)" zs += "|".join(pwds) + r")([]&; ]|$)"
self.re_pwd = re.compile(zs) self.re_pwd = re.compile(zs)
@ -3113,7 +3111,13 @@ class AuthSrv(object):
try: try:
s_vfs, s_rem = vfs.get( s_vfs, s_rem = vfs.get(
s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr s_vp,
s_un,
"r" in s_pr,
"w" in s_pr,
"m" in s_pr,
"d" in s_pr,
"g" in s_pr,
) )
except Exception as ex: except Exception as ex:
t = "removing share [%s] by [%s] to [%s] due to %r" t = "removing share [%s] by [%s] to [%s] due to %r"

View file

@ -16,6 +16,7 @@ if True: # pylint: disable=using-constant-test
if TYPE_CHECKING: if TYPE_CHECKING:
from .httpsrv import HttpSrv from .httpsrv import HttpSrv
from .svchub import SvcHub
class ExceptionalQueue(Queue, object): class ExceptionalQueue(Queue, object):
@ -50,6 +51,7 @@ class BrokerCli(object):
for example resolving httpconn.* in httpcli -- see lines tagged #mypy404 for example resolving httpconn.* in httpcli -- see lines tagged #mypy404
""" """
hub: "SvcHub"
log: "RootLogger" log: "RootLogger"
args: argparse.Namespace args: argparse.Namespace
asrv: AuthSrv asrv: AuthSrv

View file

@ -123,6 +123,7 @@ from .util import (
unescape_cookie, unescape_cookie,
unquotep, unquotep,
vjoin, vjoin,
vjoins,
vol_san, vol_san,
vroots, vroots,
vsplit, vsplit,
@ -1823,14 +1824,13 @@ class HttpCli(object):
# because lstat=true would not recurse into subfolders # because lstat=true would not recurse into subfolders
# and this is a rare case where we actually want that # and this is a rare case where we actually want that
fgen = vn.zipgen( fgen = vn.zipgen(
rem, "",
rem, rem,
set(), set(),
self.uname, self.uname,
True, True,
1, 1,
not self.args.no_scandir, not self.args.no_scandir,
wrap=False,
) )
elif depth == "0": elif depth == "0":
@ -1920,7 +1920,7 @@ class HttpCli(object):
df = {} df = {}
fgen = itertools.chain([topdir], fgen) fgen = itertools.chain([topdir], fgen)
vtop = vjoin(self.args.R, vjoin(vn.vpath, rem)) vtop = vjoins(self.args.R, vn.vpath, rem)
chunksz = 0x7FF8 # preferred by nginx or cf (dunno which) chunksz = 0x7FF8 # preferred by nginx or cf (dunno which)
@ -5163,6 +5163,16 @@ class HttpCli(object):
if items: if items:
fn = "sel-" + fn fn = "sel-" + fn
if "name" in self.ouparam:
# user-selected name for toplevel folder, or blank for none
vpath = undot(self.ouparam["name"])
elif items:
# multiselect; add all items to archive root
vpath = ""
else:
# single folder; the folder itself is the top-level item
vpath = vpath.split("/")[-1].lstrip(".") or "top"
if vn.flags.get("zipmax") and not ( if vn.flags.get("zipmax") and not (
vn.flags.get("zipmaxu") and self.uname != "*" vn.flags.get("zipmaxu") and self.uname != "*"
): ):
@ -5209,7 +5219,7 @@ class HttpCli(object):
if cfmt: if cfmt:
self.log("transcoding to [{}]".format(cfmt)) self.log("transcoding to [{}]".format(cfmt))
fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt) fgen = gfilter(fgen, self.thumbcli, self.uname, self.vpath, vpath, cfmt)
now = time.time() now = time.time()
self.dl_id = "%s:%s" % (self.ip, self.addr[1]) self.dl_id = "%s:%s" % (self.ip, self.addr[1])
@ -6443,6 +6453,7 @@ class HttpCli(object):
s_wr = "write" in req["perms"] s_wr = "write" in req["perms"]
s_get = "get" in req["perms"] s_get = "get" in req["perms"]
s_dot = "dot" in req["perms"] s_dot = "dot" in req["perms"]
# will_read, will_write, will_move, will_del, will_get
s_axs = [s_rd, s_wr, False, False, s_get] s_axs = [s_rd, s_wr, False, False, s_get]
s_axsd = s_axs + [s_dot] s_axsd = s_axs + [s_dot]
@ -7186,17 +7197,18 @@ class HttpCli(object):
dirs = [] dirs = []
files = [] files = []
ptn_hr = RE_HR ptn_hr = RE_HR
use_abs_url = ( use_abs_url = is_opds or (
not is_opds vpath and not is_ls and not is_js and not self.trailing_slash
and not is_ls
and not is_js
and not self.trailing_slash
and vpath
) )
for fn in ls_names: for fn in ls_names:
base = "" base = ""
href = fn href = fn
if use_abs_url: if use_abs_url:
if is_opds:
base = self.args.SRS
if vpath:
base += vpath + "/"
else:
base = "/" + vpath + "/" base = "/" + vpath + "/"
href = base + fn href = base + fn
@ -7237,7 +7249,7 @@ class HttpCli(object):
margin = "-" margin = "-"
sz = inf.st_size sz = inf.st_size
zd = datetime.fromtimestamp(max(0, linf.st_mtime), UTC) zd = datetime.fromtimestamp(max(0, min(2 << 36, linf.st_mtime)), UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year, zd.year,
zd.month, zd.month,
@ -7458,11 +7470,11 @@ class HttpCli(object):
if doctxt is not None: if doctxt is not None:
j2a["doc"] = doctxt j2a["doc"] = doctxt
dirs.sort(key=itemgetter("name"))
for d in dirs: for d in dirs:
d["name"] += "/" d["name"] += "/"
dirs.sort(key=itemgetter("name"))
if is_opds: if is_opds:
# OpenSearch Description format requires a full-qualified URL and a "Short Name" under 16 characters # OpenSearch Description format requires a full-qualified URL and a "Short Name" under 16 characters
# which will be the longname truncated in the template. # which will be the longname truncated in the template.
@ -7527,17 +7539,26 @@ class HttpCli(object):
] ]
j2a["opds_osd"] = "%s%s?opds&osd" % (self.args.SRS, quotep(vpath)) j2a["opds_osd"] = "%s%s?opds&osd" % (self.args.SRS, quotep(vpath))
j2a["opds_id"] = uuid.uuid5(uuid.NAMESPACE_URL, vpath + "/").urn
j2a["opds_title"] = (
(vpath.rsplit("/", 1)[-1] + "/") if vpath else self.args.bname
)
for item in dirs: for item in dirs:
href = item["href"] href = item["href"]
href += ("&" if "?" in href else "?") + "opds" href += ("&" if "?" in href else "?") + "opds"
item["href"] = href item["href"] = href
item["opds_id"] = uuid.uuid5(
uuid.NAMESPACE_URL, "%s/%s" % (vpath, item["name"])
).urn
item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),) item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),)
for item in files: for item in files:
href = item["href"] href = item["href"]
href += ("&" if "?" in href else "?") + "dl" href += ("&" if "?" in href else "?") + "dl"
item["href"] = href item["href"] = href
item["opds_id"] = uuid.uuid5(
uuid.NAMESPACE_URL, "%s/%s" % (vpath, item["name"])
).urn
item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),) item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),)
if "rmagic" in self.vn.flags: if "rmagic" in self.vn.flags:

View file

@ -700,7 +700,7 @@ class HttpSrv(object):
if not fmts: if not fmts:
continue continue
log("starting for volume /%s" % (vn.vpath,), 6) log("starting for volume /%s" % (vn.vpath,), 6)
g = vn.walk("x", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False) g = vn.walk("", "/", [], LEELOO_DALLAS, [[True]], 2, scandir, False, False)
g = gfilter2(g, self, vn.vpath, fmts.split(",")) g = gfilter2(g, self, vn.vpath, fmts.split(","))
for f in g: for f in g:
nfiles += 1 nfiles += 1

View file

@ -46,24 +46,35 @@ except:
HAVE_MUTAGEN = False HAVE_MUTAGEN = False
def have_ff(scmd: str) -> bool: def have_ff(name: str) -> bytes:
if ANYWIN: uname = name.upper()
if os.environ.get("PRTY_NO_" + uname):
return b""
ebin = os.environ.get("PRTY_%s_BIN" % (uname,))
try:
scmd = (ebin or name).decode("utf-8")
except:
scmd: str = ebin or name
if ANYWIN and not ebin:
scmd += ".exe" scmd += ".exe"
if PY2: if PY2:
print("# checking {}".format(scmd)) print("# checking %s" % (scmd,))
acmd = (scmd + " -version").encode("ascii").split(b" ") bcmd = scmd.encode("utf-8")
try: try:
sp.Popen(acmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate() sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate()
return True return bcmd
except: except:
return False return b""
else: else:
return bool(shutil.which(scmd)) return (shutil.which(scmd) or "").encode("utf-8")
HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg") HAVE_FFMPEG = have_ff("ffmpeg")
HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe") HAVE_FFPROBE = have_ff("ffprobe")
TH_BWRAP = []
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split()) CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split())
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b") CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
@ -214,17 +225,28 @@ def au_unpk(
return abspath return abspath
def bwrap(prog: bytes, ap_in: bytes, ap_out: bytes) -> list[bytes]:
if not TH_BWRAP:
return [prog]
ret = TH_BWRAP + [b"--ro-bind", prog, prog, b"--ro-bind", ap_in, ap_in]
if ap_out:
zs = ap_out.rsplit(b"/", 1)[0]
ret += [b"--bind", zs, zs]
ret.append(prog)
return ret
def ffprobe( def ffprobe(
abspath: str, timeout: int = 60 abspath: str, timeout: int = 60
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
# ffprobe -hide_banner -show_streams -show_format -- # ffprobe -hide_banner -show_streams -show_format --
cmd = [ bap = fsenc(abspath)
b"ffprobe", cmd = bwrap(HAVE_FFPROBE, bap, b"") + [
b"-hide_banner", b"-hide_banner",
b"-show_streams", b"-show_streams",
b"-show_format", b"-show_format",
b"--", b"--",
fsenc(abspath), bap,
] ]
rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200) rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)
retchk(rc, cmd, se) retchk(rc, cmd, se)

View file

@ -61,6 +61,7 @@ def gfilter(
thumbcli: ThumbCli, thumbcli: ThumbCli,
uname: str, uname: str,
vtop: str, vtop: str,
vname: str,
fmt: str, fmt: str,
) -> Generator[dict[str, Any], None, None]: ) -> Generator[dict[str, Any], None, None]:
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@ -70,7 +71,7 @@ def gfilter(
_pools[tp] = 1 _pools[tp] = 1
try: try:
for f in fgen: for f in fgen:
task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt) task = tp.submit(enthumb, thumbcli, uname, vtop, vname, f, fmt)
pend.append((task, f)) pend.append((task, f))
if pend[0][0].done() or len(pend) > CORES * 4: if pend[0][0].done() or len(pend) > CORES * 4:
task, f = pend.pop(0) task, f = pend.pop(0)
@ -130,7 +131,7 @@ def gfilter2(
try: try:
f = {"vp": vp, "st": fi[1]} f = {"vp": vp, "st": fi[1]}
task = tp.submit( task = tp.submit(
enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, f, fmt enthumb, hsrv.thumbcli, LEELOO_DALLAS, vtop, "", f, fmt
) )
pend.append((task, f)) pend.append((task, f))
if pend[0][0].done() or len(pend) > CORES * 4: if pend[0][0].done() or len(pend) > CORES * 4:
@ -152,14 +153,17 @@ def gfilter2(
def enthumb( def enthumb(
thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str thumbcli: ThumbCli, uname: str, vtop: str, vname: str, f: dict[str, Any], fmt: str
) -> dict[str, Any]: ) -> dict[str, Any]:
rem = f["vp"] rem = f["vp"]
ext = rem.rsplit(".", 1)[-1].lower() ext = rem.rsplit(".", 1)[-1].lower()
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS): if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
raise Exception() raise Exception()
if vname:
vp = vjoin(vtop, rem.split("/", 1)[1]) vp = vjoin(vtop, rem.split("/", 1)[1])
else:
vp = vjoin(vtop, rem)
vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False) vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False)
dbv, vrem = vn.get_dbv(rem) dbv, vrem = vn.get_dbv(rem)
thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt) thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt)

View file

@ -27,13 +27,23 @@ if True: # pylint: disable=using-constant-test
import typing import typing
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,
UNIX,
E,
EnvParams,
unicode,
)
from .__version__ import S_VERSION, VERSION from .__version__ import S_VERSION, VERSION
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who
from .bos import bos from .bos import bos
from .cert import ensure_cert from .cert import ensure_cert
from .fsutil import ramdisk_chk from .fsutil import ramdisk_chk
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN, TH_BWRAP
from .pwhash import HAVE_ARGON2 from .pwhash import HAVE_ARGON2
from .sutil import close_pools as sutil_close_pools from .sutil import close_pools as sutil_close_pools
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
@ -41,19 +51,20 @@ from .th_srv import (
H_PIL_AVIF, H_PIL_AVIF,
H_PIL_HEIF, H_PIL_HEIF,
H_PIL_WEBP, H_PIL_WEBP,
HAVE_FFMPEG, HAVE_DCRAW,
HAVE_FFPROBE,
HAVE_PIL, HAVE_PIL,
HAVE_RAW, HAVE_RAWPY,
HAVE_VIPS, HAVE_VIPS,
ThumbSrv, ThumbSrv,
) )
from .up2k import Up2k from .up2k import Up2k
from .util import ( from .util import (
BLOCK_SIGS,
DEF_EXP, DEF_EXP,
DEF_MTE, DEF_MTE,
DEF_MTH, DEF_MTH,
FFMPEG_URL, FFMPEG_URL,
HAVE_BWRAP,
HAVE_PSUTIL, HAVE_PSUTIL,
HAVE_SQLITE3, HAVE_SQLITE3,
HAVE_ZMQ, HAVE_ZMQ,
@ -81,6 +92,7 @@ from .util import (
odfusion, odfusion,
pybin, pybin,
read_utf8, read_utf8,
signame2int,
start_log_thrs, start_log_thrs,
start_stackmon, start_stackmon,
termsize, termsize,
@ -106,11 +118,19 @@ if PY2:
else: else:
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
try:
from queue import SimpleQueue
except:
# yuul b. alwright
from queue import Queue as SimpleQueue
VER_IDP_DB = 1 VER_IDP_DB = 1
VER_SESSION_DB = 1 VER_SESSION_DB = 1
VER_SHARES_DB = 2 VER_SHARES_DB = 2
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
class SvcHub(object): class SvcHub(object):
""" """
@ -139,13 +159,9 @@ class SvcHub(object):
self.logf: Optional[typing.TextIO] = None self.logf: Optional[typing.TextIO] = None
self.logf_base_fn = "" self.logf_base_fn = ""
self.is_dut = False # running in unittest; always False self.is_dut = False # running in unittest; always False
self.stop_req = False
self.stopping = False self.stopping = False
self.stopped = False self.stopped = False
self.reload_req = False
self.reload_mutex = threading.Lock() self.reload_mutex = threading.Lock()
self.stop_cond = threading.Condition()
self.nsigs = 3
self.retcode = 0 self.retcode = 0
self.httpsrv_up = 0 self.httpsrv_up = 0
self.qr_tsz = None self.qr_tsz = None
@ -155,6 +171,12 @@ class SvcHub(object):
self.cmon = 0 self.cmon = 0
self.tstack = 0.0 self.tstack = 0.0
self.sig_logrot = -999
self.sig_reload = -999
self.sig_stack = -999
self.nsigs = 7
self.sig = SimpleQueue()
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
if args.sss or args.s >= 3: if args.sss or args.s >= 3:
@ -288,6 +310,9 @@ class SvcHub(object):
self.log("root", "vc-age too low for copyparty.eu; will use 3 hours") self.log("root", "vc-age too low for copyparty.eu; will use 3 hours")
args.vc_age = zi args.vc_age = zi
if args.vc_sev and args.vc_sev not in CVE_SEVS:
self.log("root", "vc-sev %r invalid; will use 'low'" % (args.vc_sev,), 3)
zs = "" zs = ""
if args.th_ram_max < 0.22: if args.th_ram_max < 0.22:
zs = "generate thumbnails" zs = "generate thumbnails"
@ -398,7 +423,7 @@ class SvcHub(object):
decs.pop("vips", None) decs.pop("vips", None)
if not HAVE_PIL: if not HAVE_PIL:
decs.pop("pil", None) decs.pop("pil", None)
if not HAVE_RAW: if not HAVE_RAWPY and not HAVE_DCRAW:
decs.pop("raw", None) decs.pop("raw", None)
if not HAVE_FFMPEG or not HAVE_FFPROBE: if not HAVE_FFMPEG or not HAVE_FFPROBE:
decs.pop("ff", None) decs.pop("ff", None)
@ -985,12 +1010,20 @@ class SvcHub(object):
def after_httpsrv_up(self) -> None: def after_httpsrv_up(self) -> None:
self.up2k.init_vols() self.up2k.init_vols()
Daemon(self.sd_notify, "sd-notify") zb = os.environ.get("NOTIFY_SOCKET")
if zb:
Daemon(self.sd_notify, "sd-notify", (zb,))
zb = os.environ.get("S6_NOTIFY_FD")
if zb:
Daemon(self.s6_notify, "s6-notify", (zb,))
def _feature_test(self) -> None: def _feature_test(self) -> None:
fok = [] fok = []
fng = [] fng = []
t_ff = "transcode audio, create spectrograms, video thumbnails" t_ff = "transcode audio, create spectrograms, video thumbnails"
# fmt: off
to_check = [ to_check = [
(HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"), (HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
(HAVE_PIL, "pillow", "image thumbnails (plenty fast)"), (HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
@ -998,23 +1031,29 @@ class SvcHub(object):
(H_PIL_WEBP, "pillow-webp", "create thumbnails as webp files"), (H_PIL_WEBP, "pillow-webp", "create thumbnails as webp files"),
(HAVE_FFMPEG, "ffmpeg", t_ff + ", good-but-slow image thumbnails"), (HAVE_FFMPEG, "ffmpeg", t_ff + ", good-but-slow image thumbnails"),
(HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"), (HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"),
(HAVE_BWRAP, "bwrap", "sandbox to make ffmpeg less dangerous", not ANYWIN and not UNIX),
(HAVE_MUTAGEN, "mutagen", "read audio tags (ffprobe is better but slower)"), (HAVE_MUTAGEN, "mutagen", "read audio tags (ffprobe is better but slower)"),
(HAVE_ARGON2, "argon2", "secure password hashing (advanced users only)"), (HAVE_ARGON2, "argon2", "secure password hashing (advanced users only)"),
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"), (HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
(H_PIL_HEIF, "pillow-heif", "read .heif pics with pillow (rarely useful)"), (H_PIL_HEIF, "pillow-heif", "read .heif pics with pillow (rarely useful)"),
(H_PIL_AVIF, "pillow-avif", "read .avif pics with pillow (rarely useful)"), (H_PIL_AVIF, "pillow-avif", "read .avif pics with pillow (rarely useful)"),
(HAVE_RAW, "rawpy", "read RAW images"), (HAVE_RAWPY, "rawpy", "read RAW images"),
] (HAVE_DCRAW, "libraw", "read RAW images"),
if ANYWIN: (HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)", ANYWIN),
to_check += [
(HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)")
] ]
# fmt: on
verbose = self.args.deps verbose = self.args.deps
if verbose: if verbose:
self.log("dependencies", "") self.log("dependencies", "")
for have, feat, what in to_check: for zc in to_check:
try:
have, feat, what = zc
except:
have, feat, what, zb = zc
if not zb:
continue
lst = fok if have else fng lst = fok if have else fng
lst.append((feat, what)) lst.append((feat, what))
if verbose: if verbose:
@ -1181,6 +1220,15 @@ class SvcHub(object):
vsa = [x.upper() for x in vsa if x] vsa = [x.upper() for x in vsa if x]
setattr(al, k + "_set", set(vsa)) setattr(al, k + "_set", set(vsa))
zs = "th_bwrap"
for k in zs.split(" "):
zsl = [x for x in str(getattr(al, k)).split(" ") if x]
zbl = [x.encode("ascii", "replace") for x in zsl]
setattr(al, k + "_s", zsl)
setattr(al, k + "_b", zbl)
TH_BWRAP[:] = al.th_bwrap_b
zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip" zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
for k in zs.split(" "): for k in zs.split(" "):
vs = getattr(al, k) vs = getattr(al, k)
@ -1443,31 +1491,44 @@ class SvcHub(object):
sigs = [signal.SIGINT, signal.SIGTERM] sigs = [signal.SIGINT, signal.SIGTERM]
if not ANYWIN: if not ANYWIN:
sigs.append(signal.SIGUSR1) sigs.append(signal.SIGHUP)
for (opt, mem) in (
("logrot_sig", "sig_logrot"),
("reload_sig", "sig_reload"),
("stack_sig", "sig_stack"),
):
zs = getattr(self.args, opt)
if not zs:
continue
zi = signame2int(zs)
setattr(self, mem, zi)
try:
sigs.append(signal.Signals(zi))
except:
t = "using unknown signal %r as %s"
self.log("root", t % (zi, mem), 3)
sigs.append(zi)
for sig in sigs: for sig in sigs:
signal.signal(sig, self.signal_handler) signal.signal(sig, self.signal_handler)
if sig not in BLOCK_SIGS and BLOCK_SIGS:
BLOCK_SIGS.append(sig)
# macos hangs after shutdown on sigterm with while-sleep, if self.args.sig_thr:
# windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??) Daemon(self._signal_thr, "svchub-sig")
# linux is fine with both,
# never lucky
if ANYWIN:
# msys-python probably fine but >msys-python
Daemon(self.stop_thr, "svchub-sig")
try: try:
while not self.stop_req: while not self.stopping:
time.sleep(1) time.sleep(1)
except: except:
pass pass
self.shutdown()
# cant join; eats signals on win10 # cant join; eats signals on win10
while not self.stopped: while not self.stopped:
time.sleep(0.1) time.sleep(0.1)
else: else:
self.stop_thr() self._signal_thr()
def start_zeroconf(self) -> None: def start_zeroconf(self) -> None:
self.zc_ngen += 1 self.zc_ngen += 1
@ -1525,17 +1586,6 @@ class SvcHub(object):
self.asrv.load_sessions(True) self.asrv.load_sessions(True)
self.broker.reload_sessions() self.broker.reload_sessions()
def stop_thr(self) -> None:
while not self.stop_req:
with self.stop_cond:
self.stop_cond.wait(9001)
if self.reload_req:
self.reload_req = False
self.reload(True, True)
self.shutdown()
def kill9(self, delay: float = 0.0) -> None: def kill9(self, delay: float = 0.0) -> None:
if delay > 0.01: if delay > 0.01:
time.sleep(delay) time.sleep(delay)
@ -1548,26 +1598,42 @@ class SvcHub(object):
os.kill(os.getpid(), signal.SIGKILL) os.kill(os.getpid(), signal.SIGKILL)
def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None: def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None:
if self.stopping: if sig in (signal.SIGINT, signal.SIGTERM):
if self.nsigs <= 0: self.nsigs -= 1
if self.nsigs == 0:
try: try:
threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start() threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start()
time.sleep(0.1) time.sleep(0.1)
except: except:
pass pass
if self.nsigs <= 0:
self.kill9() self.kill9()
else:
self.nsigs -= 1
return
if not ANYWIN and sig == signal.SIGUSR1: self.sig.put(sig)
self.reload_req = True
else:
self.stop_req = True
with self.stop_cond: def _signal_thr(self) -> None:
self.stop_cond.notify_all() while not self.stopping:
sig = self.sig.get()
self._signal_handler(sig)
def _signal_handler(self, sig: int) -> None:
if sig == self.sig_logrot:
self.log("root", "signal: logrotate")
dt = datetime.now(self.tz)
self.logf_base_fn = "\t"
self._set_next_day(dt)
elif sig == self.sig_reload:
self.log("root", "signal: reload")
self.reload(True, True)
elif sig == self.sig_stack:
self.log("root", "signal: stack%s" % (alltrace(),))
else:
self.shutdown()
def shutdown(self) -> None: def shutdown(self) -> None:
if self.stopping: if self.stopping:
@ -1575,10 +1641,8 @@ class SvcHub(object):
# start_log_thrs(print, 0.1, 1) # start_log_thrs(print, 0.1, 1)
self.nsigs = 3
self.stopping = True self.stopping = True
self.stop_req = True
with self.stop_cond:
self.stop_cond.notify_all()
ret = 1 ret = 1
try: try:
@ -1878,12 +1942,8 @@ class SvcHub(object):
self.log("svchub", "cannot efficiently use multiple CPU cores") self.log("svchub", "cannot efficiently use multiple CPU cores")
return False return False
def sd_notify(self) -> None: def sd_notify(self, zb: bytes) -> None:
try: try:
zb = os.environ.get("NOTIFY_SOCKET")
if not zb:
return
addr = unicode(zb) addr = unicode(zb)
if addr.startswith("@"): if addr.startswith("@"):
addr = "\0" + addr[1:] addr = "\0" + addr[1:]
@ -1895,7 +1955,19 @@ class SvcHub(object):
sck.connect(addr) sck.connect(addr)
sck.sendall(b"READY=1") sck.sendall(b"READY=1")
except: except:
self.log("sd_notify", min_ex()) t = "NOTIFY_SOCKET=%s:\n%s"
self.log("sd-notify", t % (zb, min_ex()), 1)
def s6_notify(self, zb: bytes) -> None:
try:
fd = int(zb)
if fd < 3:
raise Exception("value < 3")
os.write(fd, b"\n")
os.close(fd)
except:
t = "S6_NOTIFY_FD=%s:\n%s"
self.log("s6-notify", t % (zb, min_ex()), 1)
def log_stacks(self) -> None: def log_stacks(self) -> None:
td = time.time() - self.tstack td = time.time() - self.tstack
@ -1914,6 +1986,7 @@ class SvcHub(object):
next_chk = 0 next_chk = 0
# self.args.vc_age = 2 / 60 # self.args.vc_age = 2 / 60
fpath = os.path.join(self.E.cfg, "vuln_advisory.json") fpath = os.path.join(self.E.cfg, "vuln_advisory.json")
minsev = CVE_SEVS.get(self.args.vc_sev, 0)
while not self.stopping: while not self.stopping:
now = time.time() now = time.time()
if now < next_chk: if now < next_chk:
@ -1957,10 +2030,13 @@ class SvcHub(object):
continue continue
try: try:
sver = "0.1"
advisories = json.loads(jtxt) advisories = json.loads(jtxt)
for adv in advisories: for adv in advisories:
if adv.get("state") == "closed": if adv.get("state") == "closed":
continue continue
if CVE_SEVS.get(adv.get("severity"), 9) < minsev:
continue
vuln = {} vuln = {}
for x in adv["vulnerabilities"]: for x in adv["vulnerabilities"]:
if x["package"]["name"].lower() == "copyparty": if x["package"]["name"].lower() == "copyparty":
@ -1978,7 +2054,6 @@ class SvcHub(object):
if self.args.vc_exit: if self.args.vc_exit:
self.sigterm() self.sigterm()
return return
else:
t = "%sok; v%s and newer is safe" t = "%sok; v%s and newer is safe"
self.log("ver-chk", t % (src, sver), 2) self.log("ver-chk", t % (src, sver), 2)
next_chk = time.time() + self.args.vc_age * 3600 - age next_chk = time.time() + self.args.vc_age * 3600 - age

View file

@ -18,7 +18,7 @@ from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .authsrv import VFS from .authsrv import VFS
from .bos import bos from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, bwrap, ffprobe, have_ff
from .util import BytesIO # type: ignore from .util import BytesIO # type: ignore
from .util import ( from .util import (
FFMPEG_URL, FFMPEG_URL,
@ -201,16 +201,22 @@ except Exception as e:
logging.warning("libvips found, but failed to load: " + str(e)) logging.warning("libvips found, but failed to load: " + str(e))
PRTY_NO_RAW = os.environ.get("PRTY_NO_RAW")
PRTY_NO_RAWPY = PRTY_NO_RAW or os.environ.get("PRTY_NO_RAWPY")
PRTY_NO_DCRAW = PRTY_NO_RAW or os.environ.get("PRTY_NO_DCRAW")
try: try:
if os.environ.get("PRTY_NO_RAW"): if PRTY_NO_RAWPY:
raise Exception() raise Exception()
HAVE_RAW = True HAVE_RAWPY = True
import rawpy import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING) logging.getLogger("rawpy").setLevel(logging.WARNING)
except: except:
HAVE_RAW = False HAVE_RAWPY = False
HAVE_DCRAW = not PRTY_NO_DCRAW and have_ff("dcraw_emu")
th_dir_cache = {} th_dir_cache = {}
@ -224,11 +230,6 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -
if not rd: if not rd:
rd = "\ntop" rd = "\ntop"
# spectrograms are never cropped; strip fullsize flag
ext = rem.split(".")[-1].lower()
if ext in ffa and fmt[:2] in ("wf", "jf", "xf"):
fmt = fmt.replace("f", "")
dcache = th_dir_cache dcache = th_dir_cache
rd_key = rd + "\n" + fmt rd_key = rd + "\n" + fmt
rd = dcache.get(rd_key) rd = dcache.get(rd_key)
@ -307,6 +308,8 @@ class ThumbSrv(object):
if ANYWIN and self.args.no_acode: if ANYWIN and self.args.no_acode:
self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3) self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3)
self.conv_raw = self._conv_rawpy if HAVE_RAWPY else self._conv_dcraw
if self.args.th_clean: if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln") Daemon(self.cleaner, "thumb.cln")
@ -467,6 +470,11 @@ class ThumbSrv(object):
zs = "th_dec th_no_webp th_no_jpg" zs = "th_dec th_no_webp th_no_jpg"
for zs in zs.split(" "): for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, getattr(self.args, zs))) ret.append("%s(%s)\n" % (zs, getattr(self.args, zs)))
zs = "th_spec_fl"
for zs in zs.split(" "):
v = getattr(self.args, zs)
if v:
ret.append("%s(%s)\n" % (zs, v))
zs = "th_qv th_qvx thsize th_spec_p convt" zs = "th_qv th_qvx thsize th_spec_p convt"
for zs in zs.split(" "): for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, vn.flags.get(zs))) ret.append("%s(%s)\n" % (zs, vn.flags.get(zs)))
@ -753,8 +761,39 @@ class ThumbSrv(object):
self.conv_image_vips(_loader, tpath, fmt, vn) self.conv_image_vips(_loader, tpath, fmt, vn)
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def _conv_dcraw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath) self.wait4ram(0.6, tpath)
bap = fsenc(abspath)
# fmt: off
cmd = bwrap(HAVE_DCRAW, bap, b"") + [
b"-h", # halfsize
b"-o", b"1", # srgb
b"-s", b"0", # first frame
b"-Z", b"-", # to stdout
bap,
]
# fmt: on
p = sp.Popen(cmd, stdout=sp.PIPE)
try:
if HAVE_PIL:
self.conv_image_pil(Image.open(p.stdout), tpath, fmt, vn)
elif HAVE_VIPS:
ppm, _ = p.communicate(timeout=vn.flags["convt"])
def _loader(w: int, kw: dict) -> Any:
return pyvips.Image.thumbnail_buffer(ppm, w, **kw)
self.conv_image_vips(_loader, tpath, fmt, vn)
else:
raise Exception(
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
)
finally:
if p and p.poll() is None:
p.kill()
def _conv_rawpy(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.6, tpath)
with rawpy.imread(abspath) as raw: with rawpy.imread(abspath) as raw:
thumb = raw.extract_thumb() thumb = raw.extract_thumb()
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"): if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
@ -819,16 +858,15 @@ class ThumbSrv(object):
res = self.getres(vn, fmt) res = self.getres(vn, fmt)
bscale = scale.format(*list(res)).encode("utf-8") bscale = scale.format(*list(res)).encode("utf-8")
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner" b"-hide_banner"
] ] + seek + [
cmd += seek b"-i", bap_in,
cmd += [
b"-i", fsenc(abspath),
b"-map", imap, b"-map", imap,
b"-vf", bscale, b"-vf", bscale,
b"-frames:v", b"1", b"-frames:v", b"1",
@ -836,15 +874,15 @@ class ThumbSrv(object):
] ]
# fmt: on # fmt: on
self._ffmpeg_im_o(tpath, vn, cmd) self._ffmpeg_im_o(bap_out, vn, cmd)
def _ffmpeg_im_o(self, tpath: str, vn: VFS, cmd: list[bytes]) -> None: def _ffmpeg_im_o(self, tpath: bytes, vn: VFS, cmd: list[bytes]) -> None:
if tpath.endswith(".jpg"): if tpath.endswith(b".jpg"):
cmd += [ cmd += [
b"-q:v", b"-q:v",
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=?? FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
] ]
elif tpath.endswith(".jxl"): elif tpath.endswith(b".jxl"):
cmd += [ cmd += [
b"-q:v", b"-q:v",
unicode(vn.flags["th_qvx"]).encode("ascii"), # default=?? unicode(vn.flags["th_qvx"]).encode("ascii"), # default=??
@ -859,7 +897,7 @@ class ThumbSrv(object):
b"6", # default=4, 0=fast, 6=max b"6", # default=4, 0=fast, 6=max
] ]
cmd += [fsenc(tpath)] cmd.append(tpath)
self._run_ff(cmd, vn, "convt") self._run_ff(cmd, vn, "convt")
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None: def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
@ -959,20 +997,21 @@ class ThumbSrv(object):
b",showwavespic=s=2048x64:colors=white" b",showwavespic=s=2048x64:colors=white"
b",convolution=1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 -1 1 -1 5 -1 1 -1 1" # idk what im doing but it looks ok b",convolution=1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 -1 1 -1 5 -1 1 -1 1" # idk what im doing but it looks ok
) )
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
b"-filter_complex", flt, b"-filter_complex", flt,
b"-frames:v", b"1", b"-frames:v", b"1",
] ]
# fmt: on # fmt: on
cmd += [fsenc(tpath)] cmd.append(bap_out)
self._run_ff(cmd, vn, "convt") self._run_ff(cmd, vn, "convt")
if "pngquant" in vn.flags: if "pngquant" in vn.flags:
@ -1039,28 +1078,32 @@ class ThumbSrv(object):
except: except:
self.untemp[tpath] = [infile] self.untemp[tpath] = [infile]
bap_in = fsenc(abspath)
bap_out = fsenc(infile)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-ac", b"1", b"-ac", b"1",
b"-ar", b"48000", b"-ar", b"48000",
b"-sample_fmt", b"s16", b"-sample_fmt", b"s16",
b"-t", b"900", b"-t", b"900",
b"-y", fsenc(infile), b"-y", bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "convt") self._run_ff(cmd, vn, "convt")
fscale = ":fscale=log" if self.args.th_spec_fl else ""
fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
if "3" in fmt: if "3" in fmt:
fc += "1280x1024,crop=1420:1056:70:48[o]" fc += "1280x1024%s,crop=1420:1056:70:48[o]" % fscale
else: else:
fc += "640x512,crop=780:544:70:48[o]" fc += "640x512%s,crop=780:544:70:48[o]" % fscale
if self.args.th_ff_swr: if self.args.th_ff_swr:
fco = ":filter_size=128:cutoff=0.877" fco = ":filter_size=128:cutoff=0.877"
@ -1069,20 +1112,22 @@ class ThumbSrv(object):
fc = fc.format(fco) fc = fc.format(fco)
bap_in = fsenc(infile)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(infile), b"-i", bap_in,
b"-filter_complex", fc.encode("utf-8"), b"-filter_complex", fc.encode("utf-8"),
b"-map", b"[o]", b"-map", b"[o]",
b"-frames:v", b"1", b"-frames:v", b"1",
] ]
# fmt: on # fmt: on
self._ffmpeg_im_o(tpath, vn, cmd) self._ffmpeg_im_o(bap_out, vn, cmd)
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
quality = self.args.q_mp3.lower() quality = self.args.q_mp3.lower()
@ -1101,24 +1146,26 @@ class ThumbSrv(object):
qk = b"-q:a" qk = b"-q:a"
qv = quality[1:].encode("ascii") qv = quality[1:].encode("ascii")
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# extremely conservative choices for output format # extremely conservative choices for output format
# (always 2ch 44k1) because if a device is old enough # (always 2ch 44k1) because if a device is old enough
# to not support opus then it's probably also super picky # to not support opus then it's probably also super picky
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
] + self.big_tags(rawtags) + [ ] + self.big_tags(rawtags) + [
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-ar", b"44100", b"-ar", b"44100",
b"-ac", b"2", b"-ac", b"2",
b"-c:a", b"libmp3lame", b"-c:a", b"libmp3lame",
qk, qv, qk, qv,
fsenc(tpath) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
@ -1134,16 +1181,18 @@ class ThumbSrv(object):
self.log("conv2 flac", 6) self.log("conv2 flac", 6)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-c:a", b"flac", b"-c:a", b"flac",
fsenc(tpath) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
@ -1169,16 +1218,18 @@ class ThumbSrv(object):
self.log("conv2 wav", 6) self.log("conv2 wav", 6)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-c:a", codec, b"-c:a", codec,
fsenc(tpath) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
@ -1230,19 +1281,21 @@ class ThumbSrv(object):
except: except:
pass pass
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
] + tagset + [ ] + tagset + [
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-ac", ac, b"-ac", ac,
] + benc + [ ] + benc + [
b"-f", container, b"-f", container,
fsenc(tpath) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
@ -1271,19 +1324,21 @@ class ThumbSrv(object):
self.log("conv2 caf-tmp [%s]" % (enc,), 6) self.log("conv2 caf-tmp [%s]" % (enc,), 6)
benc = enc.encode("ascii").split(b" ") benc = enc.encode("ascii").split(b" ")
bap_in = fsenc(abspath)
bap_out = fsenc(tmp_opus)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
b"-map_metadata", b"-1", b"-map_metadata", b"-1",
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-ac", b"2", b"-ac", b"2",
] + benc + [ ] + benc + [
b"-f", b"opus", b"-f", b"opus",
fsenc(tmp_opus) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
@ -1297,20 +1352,21 @@ class ThumbSrv(object):
if dur < 20 or sz < 256 * 1024: if dur < 20 or sz < 256 * 1024:
zs = bq.decode("ascii") zs = bq.decode("ascii")
self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6) self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(abspath), b"-i", bap_in,
b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix", b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix",
b"-map_metadata", b"-1", b"-map_metadata", b"-1",
b"-ac", b"2", b"-ac", b"2",
b"-c:a", b"libopus", b"-c:a", b"libopus",
b"-b:a", bq, b"-b:a", bq,
b"-f", b"caf", b"-f", b"caf",
fsenc(tpath) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
@ -1318,18 +1374,19 @@ class ThumbSrv(object):
else: else:
# simple remux should be safe # simple remux should be safe
self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6) self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6)
bap_in = fsenc(tmp_opus)
bap_out = fsenc(tpath)
# fmt: off # fmt: off
cmd = [ cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
b"ffmpeg",
b"-nostdin", b"-nostdin",
b"-v", b"error", b"-v", b"error",
b"-hide_banner", b"-hide_banner",
b"-i", fsenc(tmp_opus), b"-i", bap_in,
b"-map_metadata", b"-1", b"-map_metadata", b"-1",
b"-map", b"0:a:0", b"-map", b"0:a:0",
b"-c:a", b"copy", b"-c:a", b"copy",
b"-f", b"caf", b"-f", b"caf",
fsenc(tpath) bap_out,
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)

View file

@ -22,6 +22,7 @@ from .util import (
quotep, quotep,
s3dec, s3dec,
vjoin, vjoin,
vjoins,
) )
if HAVE_SQLITE3: if HAVE_SQLITE3:
@ -437,7 +438,7 @@ class U2idx(object):
if rd.startswith("//") or fn.startswith("//"): if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn) rd, fn = s3dec(rd, fn)
vp = vjoin(vjoin(vtop, rd), fn) vp = vjoins(vtop, rd, fn)
if vp in seen_rps: if vp in seen_rps:
continue continue

View file

@ -70,6 +70,7 @@ from .util import (
ub64enc, ub64enc,
unhumanize, unhumanize,
vjoin, vjoin,
vjoins,
vsplit, vsplit,
w8b64dec, w8b64dec,
w8b64enc, w8b64enc,
@ -102,7 +103,7 @@ 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,m4b,m4r,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub" zsg = "aif,aiff,alac,ape,flac,m4a,m4b,m4r,mka,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub"
ACV_EXTS = set(zsg.split(",")) ACV_EXTS = set(zsg.split(","))
zsg = "nohash noidx xdev xvol" zsg = "nohash noidx xdev xvol"
@ -3037,7 +3038,7 @@ class Up2k(object):
raise Pebkac(500, "too many xbu relocs, giving up") raise Pebkac(500, "too many xbu relocs, giving up")
ptop = cj["ptop"] ptop = cj["ptop"]
if not self.register_vpath(ptop, cj["vcfg"]): if not self.register_vpath(ptop, cj.pop("vcfg")):
if ptop not in self.registry: if ptop not in self.registry:
raise Pebkac(410, "location unavailable") raise Pebkac(410, "location unavailable")
@ -3077,7 +3078,7 @@ class Up2k(object):
) )
zi = cj["lmod"] zi = cj["lmod"]
bad_mt = zi <= 0 or zi > 0xAAAAAAAA bad_mt = zi <= 0 or zi > (2 << 36)
if bad_mt or vfs.flags.get("up_ts", "") == "fu": if bad_mt or vfs.flags.get("up_ts", "") == "fu":
# force upload time rather than last-modified # force upload time rather than last-modified
cj["lmod"] = int(time.time()) cj["lmod"] = int(time.time())
@ -3190,7 +3191,7 @@ class Up2k(object):
c2 = None c2 = None
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 % ("/" + vjoins(vfs.vpath, dp_dir, dp_fn)))
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"]) self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
if c2 and c2 != cur: if c2 and c2 != cur:
c2.connection.commit() c2.connection.commit()
@ -3280,7 +3281,10 @@ class Up2k(object):
vfs.lim.nup(cj["addr"]) vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"]) vfs.lim.bup(cj["addr"], cj["size"])
if "done" not in job: if "rvp0" in job and wark in reg:
# xbu reloc; accept wrong path
job["addr"] = cj["addr"]
elif "done" not in job:
self.log("unfinished:\n %r\n %r" % (src, dst)) self.log("unfinished:\n %r\n %r" % (src, dst))
err = "partial upload exists at a different location; please resume uploading here instead:\n" err = "partial upload exists at a different location; please resume uploading here instead:\n"
err += "/" + quotep(vsrc) + " " err += "/" + quotep(vsrc) + " "
@ -3370,9 +3374,9 @@ class Up2k(object):
x = pathmod(self.vfs, dst, vp, hr["reloc"]) x = pathmod(self.vfs, dst, vp, hr["reloc"])
if x: if x:
ud1 = (vfs.vpath, job["prel"], job["name"]) ud1 = (vfs.vpath, job["prel"], job["name"])
job["rvp0"] = vjoins(*ud1)
pdir, _, job["name"], (vfs, rem) = x pdir, _, job["name"], (vfs, rem) = x
dst = os.path.join(pdir, job["name"]) dst = os.path.join(pdir, job["name"])
job["vcfg"] = vfs.flags
job["ptop"] = vfs.realpath job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath job["vtop"] = vfs.vpath
job["prel"] = rem job["prel"] = rem
@ -3380,8 +3384,19 @@ class Up2k(object):
ud2 = (vfs.vpath, job["prel"], job["name"]) ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2: if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4)) # print(json.dumps(job, sort_keys=True, indent=4))
job["vcfg"] = vfs.flags
job["hash"] = cj["hash"] job["hash"] = cj["hash"]
self.log("xbu reloc1:%d..." % (depth,), 6) t = "xbu reloc1=%d ptop=%r vtop=%r prel=%r name=%r"
t = t % (
depth,
job["ptop"],
job["vtop"],
job["prel"],
job["name"],
)
self.log(t, 6)
zs = djoin(job["ptop"], job["prel"])
bos.makedirs(zs, vf=vfs.flags)
return self._handle_json(job, depth + 1) return self._handle_json(job, depth + 1)
job["name"] = self._untaken(pdir, job, now) job["name"] = self._untaken(pdir, job, now)
@ -5263,15 +5278,25 @@ class Up2k(object):
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"]) x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
if x: if x:
ud1 = (vfs.vpath, job["prel"], job["name"]) ud1 = (vfs.vpath, job["prel"], job["name"])
job["rvp0"] = vjoins(*ud1)
pdir, _, job["name"], (vfs, rem) = x pdir, _, job["name"], (vfs, rem) = x
job["vcfg"] = vf = vfs.flags vf = vfs.flags
job["ptop"] = vfs.realpath job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath job["vtop"] = vfs.vpath
job["prel"] = rem job["prel"] = rem
job["name"] = sanitize_fn(job["name"]) job["name"] = sanitize_fn(job["name"])
ud2 = (vfs.vpath, job["prel"], job["name"]) ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2: if ud1 != ud2:
self.log("xbu reloc2:%d..." % (depth,), 6) job["vcfg"] = vf
t = "xbu reloc2=%d ptop=%r vtop=%r prel=%r name=%r" % (
depth,
job["ptop"],
job["vtop"],
job["prel"],
job["name"],
)
self.log(t, 6)
bos.makedirs(djoin(job["ptop"], job["prel"]), vf=vf)
return self._handle_json(job, depth + 1) return self._handle_json(job, depth + 1)
job["name"] = self._untaken(pdir, job, job["t0"]) job["name"] = self._untaken(pdir, job, job["t0"])

View file

@ -326,7 +326,14 @@ except:
BITNESS = struct.calcsize("P") * 8 BITNESS = struct.calcsize("P") * 8
CAN_SIGMASK = not (ANYWIN or PY2 or GRAAL) try:
if ANYWIN or PY2 or GRAAL or not hasattr(signal, "pthread_sigmask"):
raise Exception()
BLOCK_SIGS = [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGUSR1]
CAN_SIGMASK = True
except:
BLOCK_SIGS = []
CAN_SIGMASK = False
RE_ANSI = re.compile("\033\\[[^mK]*[mK]") RE_ANSI = re.compile("\033\\[[^mK]*[mK]")
@ -516,7 +523,7 @@ image heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf
image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf
image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f
audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mka=x-matroska mpc=musepack aif=aiff au=basic qcp=qcelp
video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t
video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr
font ttc=collection font ttc=collection
@ -650,6 +657,14 @@ if EXE:
pass pass
try:
if PY2 or ANYWIN:
raise Exception()
HAVE_BWRAP = shutil.which("bwrap")
except:
HAVE_BWRAP = ""
def py_desc() -> str: def py_desc() -> str:
interp = platform.python_implementation() interp = platform.python_implementation()
py_ver = ".".join([str(x) for x in sys.version_info]) py_ver = ".".join([str(x) for x in sys.version_info])
@ -832,10 +847,8 @@ class Daemon(threading.Thread):
self.start() self.start()
def run(self): def run(self):
if CAN_SIGMASK: if BLOCK_SIGS:
signal.pthread_sigmask( signal.pthread_sigmask(signal.SIG_BLOCK, BLOCK_SIGS)
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
)
self.fun(*self.a, **self.ka) self.fun(*self.a, **self.ka)
@ -1635,6 +1648,16 @@ def expand_osenv_cs(txt) -> str:
raise Exception(t) raise Exception(t)
def signame2int(txt: str) -> int:
try:
return int(txt)
except:
txt = txt.upper()
if not txt.startswith("SIG"):
txt = "SIG" + txt
return int(getattr(signal, txt))
def rice_tid() -> str: def rice_tid() -> str:
tid = threading.current_thread().ident tid = threading.current_thread().ident
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])
@ -1768,9 +1791,7 @@ def log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> No
def _sigblock(): def _sigblock():
signal.pthread_sigmask( signal.pthread_sigmask(signal.SIG_BLOCK, BLOCK_SIGS)
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
)
sigblock = _sigblock if CAN_SIGMASK else noop sigblock = _sigblock if CAN_SIGMASK else noop
@ -2666,6 +2687,10 @@ def vjoin(rd: str, fn: str) -> str:
return rd or fn return rd or fn
def vjoins(*a: str) -> str:
return "/".join([x for x in a if x])
# url-join # url-join
def ujoin(rd: str, fn: str) -> str: def ujoin(rd: str, fn: str) -> str:
if rd and fn: if rd and fn:

View file

@ -1701,7 +1701,7 @@ mpl.init_ac2();
var re_m3u = /\.(m3u8?)$/i; var re_m3u = /\.(m3u8?)$/i;
var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4[abr]|mp3|oga|ogg|opus|wav)$/i : /\.(aac|flac|m4[abr]|mp3|wav)$/i, var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4[abr]|mp3|oga|ogg|opus|wav)$/i : /\.(aac|flac|m4[abr]|mp3|wav)$/i,
re_au_vid = /\.(3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i, re_au_vid = /\.(3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i,
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|b[cfr]stm|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i; re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|b[cfr]stm|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mka|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i;
// extract songs + add play column // extract songs + add play column
@ -3760,7 +3760,7 @@ function sortfiles(nodes) {
if ((v + '').indexOf('<a ') === 0) if ((v + '').indexOf('<a ') === 0)
v = v.split('>')[1]; v = v.split('>')[1];
else if (name == "href" && v) else if (name == "href" && v)
v = uricom_dec(v); v = uri2txt(v, true);
nodes[b]._sv = v nodes[b]._sv = v
} }
@ -4145,6 +4145,8 @@ var fileman = (function () {
if (this.textContent == 'write-only') if (this.textContent == 'write-only')
for (var a = 0; a < pbtns.length; a++) for (var a = 0; a < pbtns.length; a++)
clmod(pbtns[a], 'on', pbtns[a].textContent == 'write'); clmod(pbtns[a], 'on', pbtns[a].textContent == 'write');
if (this.textContent == 'get' && clgot(this, 'on') && has(perms, 'read'))
clmod(pbtns[0], 'on');
} }
clmod(pbtns[0], 'on', 1); clmod(pbtns[0], 'on', 1);
@ -7871,7 +7873,7 @@ var treectl = (function () {
delete res['a']; delete res['a'];
var keys = Object.keys(res); var keys = Object.keys(res);
for (var a = 0; a < keys.length; a++) for (var a = 0; a < keys.length; a++)
keys[a] = [uricom_dec(keys[a]), keys[a]]; keys[a] = [uri2txt(keys[a]), keys[a]];
if (ENATSORT) if (ENATSORT)
keys.sort(function (a, b) { return NATSORT.compare(a[0], b[0]); }); keys.sort(function (a, b) { return NATSORT.compare(a[0], b[0]); });
@ -8033,12 +8035,17 @@ function apply_perms(res) {
a.style.display = ''; a.style.display = '';
tt.att(QS('#ops')); tt.att(QS('#ops'));
var v_perms = perms.slice(0);
var have_read = has(perms, 'read');
if (have_read)
apop(v_perms, 'get');
for (var a = 0; a < chk.length; a++) for (var a = 0; a < chk.length; a++)
if (has(perms, chk[a])) if (has(v_perms, chk[a]))
axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1)); axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1));
axs = axs.join('-'); axs = axs.join('-');
if (perms.length == 1) { if (v_perms.length == 1) {
aclass = ' class="warn">'; aclass = ' class="warn">';
axs += '-Only'; axs += '-Only';
} }
@ -8080,7 +8087,6 @@ function apply_perms(res) {
document.body.setAttribute('perms', perms.join(' ')); document.body.setAttribute('perms', perms.join(' '));
var have_write = has(perms, "write"), var have_write = has(perms, "write"),
have_read = has(perms, "read"),
de = document.documentElement, de = document.documentElement,
tds = QSA('#u2conf td'); tds = QSA('#u2conf td');
@ -8979,6 +8985,13 @@ var msel = (function () {
return; return;
} }
if (this.status == 405) {
tb.value = '';
sf.textContent = 'already existed';
treectl.goto(this.vp + uricom_enc(this.dn) + '/', true);
return tree_scrollto();
}
xhrchk(this, L.fd_xe1, L.fd_xe2); xhrchk(this, L.fd_xe1, L.fd_xe2);
if (this.status !== 201) { if (this.status !== 201) {
@ -10030,7 +10043,7 @@ function reload_browser() {
if (e.target.closest('#widget,#ops,.opview,.doc')) return; if (e.target.closest('#widget,#ops,.opview,.doc')) return;
if (e.target.closest('#gfiles')) if (e.target.closest('#gfiles'))
ebi('gfiles').style.userSelect = "none" ebi('gfiles').style.userSelect = "none";
var pos = getpp(e); var pos = getpp(e);
startx = pos.x; startx = pos.x;
@ -10071,7 +10084,9 @@ function reload_browser() {
return; return;
} }
if (!dragging && dist > mvthresh && !window.getSelection().toString()) { if (!dragging && dist > mvthresh && !window.getSelection().toString()) {
if (fwrap = e.target.closest('#wrap')) if (e.target instanceof Element)
fwrap = e.target.closest('#wrap');
if (fwrap)
fwrap.style.userSelect = 'none'; fwrap.style.userSelect = 'none';
else return; else return;
start_drag(); start_drag();
@ -10115,7 +10130,8 @@ function reload_browser() {
window.addEventListener('dragstart', function(e) { window.addEventListener('dragstart', function(e) {
if (treectl.dsel && (is_selma || dragging)) { if (treectl.dsel && (is_selma || dragging)) {
e.preventDefault(); if (!QS('body.bbox-open'))
ev(e);
} }
}); });
} }

View file

@ -1,10 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
<id>{{ opds_id }}</id>
<title>{{ opds_title | e }}</title>
<link rel="search" <link rel="search"
href="{{ opds_osd | e }}" href="{{ opds_osd | e }}"
type="application/opensearchdescription+xml"/> type="application/opensearchdescription+xml"/>
{%- for d in dirs %} {%- for d in dirs %}
<entry> <entry>
<id>{{ d.opds_id }}</id>
<title>{{ d.name | e }}</title> <title>{{ d.name | e }}</title>
<link rel="subsection" <link rel="subsection"
href="{{ d.href | e }}" href="{{ d.href | e }}"
@ -14,6 +17,7 @@
{%- endfor %} {%- endfor %}
{%- for f in files %} {%- for f in files %}
<entry> <entry>
<id>{{ f.opds_id }}</id>
<title>{{ f.name | e }}</title> <title>{{ f.name | e }}</title>
<updated>{{ f.iso8601 }}</updated> <updated>{{ f.iso8601 }}</updated>
<link rel="http://opds-spec.org/acquisition" <link rel="http://opds-spec.org/acquisition"

View file

@ -233,7 +233,7 @@
<h1>partyfuse</h1> <h1>partyfuse</h1>
<p> <p>
<a href="{{ r }}/.cpr/a/partyfuse.py">partyfuse.py</a> -- fast, read-only, <a href="{{ r }}/.cpr/a/partyfuse.py">partyfuse.py</a> -- fast, read-only,
needs <a href="{{ r }}/.cpr/w/deps/fuse.py">fuse.py</a> in the same folder, needs <a href="{{ r }}/.cpr/w/deps/mfusepy.py">mfusepy.py</a> in the same folder,
<span class="os win">needs <a href="https://winfsp.dev/rel/">winfsp</a></span> <span class="os win">needs <a href="https://winfsp.dev/rel/">winfsp</a></span>
<span class="os lin">doesn't need root</span> <span class="os lin">doesn't need root</span>
</p> </p>

View file

@ -877,6 +877,16 @@ function url_enc(txt) {
return ret.join('/'); return ret.join('/');
} }
function uri2txt(txt, unslash) {
try {
txt = decodeURIComponent(txt.split('?')[0]);
}
catch (ex) {
console.log("ucd-err [" + txt + "]");
}
return unslash ? txt.replace(/\/$/, '') : txt;
}
function uricom_dec(txt) { function uricom_dec(txt) {
try { try {

View file

@ -1,3 +1,63 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0424-2222 `v1.20.14` autolocalization
## 🧪 new features
* #1410 #376 #1224 new option `--glang` to autoselect UI-translation based on webbrowser's language (thx @stackxp!) ec3e0e7e
* #1407 #1384 option to automatically switch between list-view and grid-view depending on folder contents (thx @icxes!) 822fa718 660ed7a9 961a2737
* #1447 audioplayer can now play bcstm / bfstm / brstm files (nintendo 3ds/wii bgm) 3a9ff67a
* #1389 add 1000-based filesize-units in addition to 1024-based 43773f2c
* #1395 [reloc-by-wark](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#more-upload-stuff), a pair of hooks to rename incoming uploads to a hash of the file contents 1e7de5d1
* option [--rlo](https://copyparty.eu/cli/#rlo-help-page) to change the logrotate-counter for [-lo](https://copyparty.eu/cli/#g-lo) 8b986888
* add `--certkey` to specify certificate and key as separate files 8c7cdf85
* but the built-in HTTPS server should [still not be trusted](https://github.com/9001/copyparty/#https)
* config-files can now use OS environment-variables anywhere in the `[global]` config section cbd82b65 e52bbed8
* by default, only the syntax `${VAR}` is supported, not `$VAR` or `%VAR%`
* previously, a small handful of global-options already supported this (`c lo hist dbpath ssl_log`), but they also supported the `$VAR` syntax, which is no longer the case
* if the old `$VAR` syntax is detected, copyparty will crash on startup, suggesting the following remedies (choose one!) in the log:
1. update the config-value to the new `${VAR}` syntax (recommended)
2. allow the old syntax with global-option `--env-expand 1` (risky)
3. ignore the old syntax and only expand the new syntax with global-option `--env-expand 2`
4. disable all environment-variable expansions with `PRTY_NO_ENVEXPAND=1`
## 🩹 bugfixes
* #1437 webdav clients can now PROPFIND a file with `depth: infinite` which at least [webdav4](https://github.com/skshetry/webdav4) does e00f2b46
* #1392 navigating into a subfolder using a `dks` [dirkey](https://github.com/9001/copyparty/#dirkeys) (default-disabled) could fail 228c3dfa
* #1446 #1330 #1362 fix some small edgecases with the rightclick-menu (thx @icxes!) 874e0e7a
* #1403 #1396 audioplayer: fix ui-crash when folder contains an m3u-file and sort-order is changed during playback (thx @icxes!) 198f631a
* #1428 #1427 when `--magic` was enabled, nameless uploads of textfiles would get the file-extension `.ssa` instead of `.txt` (thx @Scotsguy!) ed516ddc
* #1449 on some filesystems, the tail/follow function would spam the log with `reopened at byte XXX` 81730189
* #1401 on windows, a spec-violating basic-upload could delay that upload by a few seconds 6fb1287e
* on macOS, u2c would clear the terminal on exit, even with `-ns` 238887c7
* audio-files in a videofile trenchcoat did not thumbnail correctly 1066dc39
## 🔧 other changes
* #1387 added gentoo packaging (thx @mid-kid!) fb5384f4
* #1425 improved FreeBSD / OpenBSD support (thx @chilledfrogs!) f5613187 745d82fa
* #1352 new handler: [fail2ban](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/404-to-fail2ban.py) (thx @Lomaiin!) 26e663d1
* improve errormessage when the server's OS-HDD blips out of existence d1517d0c
* #1439 improve IPv6 autoban IP-range (thx @SnowSquire!) f6dc1e29
* ensure opus transcodes will at most have 2 audio channels (stereo) b31f2902
* #1417 smb-server: probably add IPv6 support a5d859d2
* `--list-nics` and `--list-ips` to show autodetected network-adapters and IPs 8d4363d1
* docs:
* nixos module-override example (thx @Scotsguy!) 0b16e875
* make it even more obvious that `--allow-csrf` is a bad idea 9a724b01
* mention `--urlform get` to disable message-to-serverlog ac05b4f1
* readme: improve [shadowing](https://github.com/9001/copyparty#shadowing) phrasing 003c68d0
* [devnotes](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#dependencies): explain the vendored dependencies 971f8ef9
## 🌠 fun facts
* this release includes [code](https://github.com/9001/copyparty/commit/cbd82b65) written at [abs(unit)](https://a.ocv.me/pub/g/nerd-stuff/abs-unit.jpg)
* btw that pdp had an IPv6 lease and browsed the internet :^)
* hasn't connected to copyparty though (yet...)
* this release was powered by [一体いつから (TaKo Hardcore bootleg)](https://soundcloud.com/takomusiccc/tako-hardcore-bootleg) followed by [Fighting My Way (YUPPUN Hardcore Remix)](https://soundcloud.com/yuppun/fightingmyway) (shd is a good dj)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0323-0328 `v1.20.13` dothidden # 2026-0323-0328 `v1.20.13` dothidden

View file

@ -374,7 +374,7 @@ some third-party code has been vendored into the git repo; some for convenience,
* `dnslib` (MIT) may be deleted and replaced with a systemwide install of the original [dnslib](https://github.com/paulc/dnslib/), HOWEVER: * `dnslib` (MIT) may be deleted and replaced with a systemwide install of the original [dnslib](https://github.com/paulc/dnslib/), HOWEVER:
* will cause problems for mDNS in some network environments; 6c1cf68bca7376c6291c3cfe710ebd5bd5ed3e6c + 94d1924fa97e5faaf1ebfd85cae73faebcb89fa1 * will cause problems for mDNS in some network environments; 6c1cf68bca7376c6291c3cfe710ebd5bd5ed3e6c + 94d1924fa97e5faaf1ebfd85cae73faebcb89fa1
* inside the folder `/copyparty/web/deps` (only in distributed archives/builds) is [fuse.py](https://github.com/fusepy/fusepy/blob/master/fuse.py), to make it downloadable from the connect-page on the web-ui * inside the folder `/copyparty/web/deps` (only in distributed archives/builds) is [mfusepy.py](https://github.com/mxmlnkn/mfusepy/blob/master/mfusepy.py) (sizegolfed, no important changes), to make it downloadable from the connect-page on the web-ui
* inside the folder `/copyparty/web` (only in distributed archives/builds) is a collection of javascript libraries (produced by [deps-docker](https://github.com/9001/copyparty/tree/hovudstraum/scripts/deps-docker)) which are used clientside by the web-UI: * inside the folder `/copyparty/web` (only in distributed archives/builds) is a collection of javascript libraries (produced by [deps-docker](https://github.com/9001/copyparty/tree/hovudstraum/scripts/deps-docker)) which are used clientside by the web-UI:
* [marked.js](https://github.com/markedjs/marked/releases) (MIT) powers the markdown editor, and has been [patched](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/marked-ln.patch) to include the line-numbers of each input line, to enable scroll-sync between the editor and the preview-pane. This patch is [not strictly necessary anymore](https://github.com/markedjs/marked/issues/2134) but I haven't gotten around to making the change yet * [marked.js](https://github.com/markedjs/marked/releases) (MIT) powers the markdown editor, and has been [patched](https://github.com/9001/copyparty/blob/hovudstraum/scripts/deps-docker/marked-ln.patch) to include the line-numbers of each input line, to enable scroll-sync between the editor and the preview-pane. This patch is [not strictly necessary anymore](https://github.com/markedjs/marked/issues/2134) but I haven't gotten around to making the change yet
@ -447,6 +447,12 @@ build the sfx using any of the following examples:
./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor ./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor
``` ```
on macos, you need to download several GNU utilities before building:
```zsh
brew install gsed gnu-tar findutils coreutils
```
## build from release tarball ## build from release tarball
@ -494,9 +500,13 @@ to get started, first `cd` into the `scripts` folder
* if you want to build a pypi package, now run `./make-pypi-release.sh d` * if you want to build a pypi package, now run `./make-pypi-release.sh d`
* if you want to build a docker-image, you have two options: * if you want to build a docker-image, you have two options:
* if you want to use podman to build all docker-images for all supported architectures, now run `(cd docker; ./make.sh hclean; ./make.sh hclean pull img)` * if you want to use podman to build all docker-images for all supported architectures, now run `(cd docker; make -C base; ./make.sh hclean; ./make.sh hclean pull img)`
* if you want to use docker to build all docker-images for your native architecture, now run `sudo make -C docker` * if you want to use docker to build just the `ac` docker-image for your native architecture, now run `sudo make -C docker`
* to use docker to build something other than `ac`, list the image variants you want; `sudo make -C docker min im ac iv dj`
* if you want to do something else, please take a look at `docker/make.sh` or `docker/Makefile` for inspiration * if you want to do something else, please take a look at `docker/make.sh` or `docker/Makefile` for inspiration
* beware of the following:
* if you build with docker, you get the [stock alpine-provided ffmpeg](https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md), which makes the docker-image almost twice as big
* if you build with podman (make.sh) you get the copyparty-official legally-comfy custom ffmpeg, but the first build takes about 2-3 hours longer
* if you want to build the windows exe, first grab some snacks and a beer, [you'll need it](https://github.com/9001/copyparty/tree/hovudstraum/scripts/pyinstaller) * if you want to build the windows exe, first grab some snacks and a beer, [you'll need it](https://github.com/9001/copyparty/tree/hovudstraum/scripts/pyinstaller)

View file

@ -36,7 +36,7 @@ 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 https://github.com/mxmlnkn/mfusepy
C: 2012 Giorgos Verigakis C: 2012 Giorgos Verigakis
L: ISC L: ISC

64
docs/th-raw.txt Normal file
View file

@ -0,0 +1,64 @@
FS=/home/ed/Pictures/rawsamples-ch # https://rawsamples.ch/index.php/en/ (the 7z)
find $FS -type f | sed -r 's/(.*)\.(.*)/\2 \1.\2/' | sort | tr '[:upper:]' '[:lower:]' | uniq -cw16 | sort -n | awk '{printf"%s ",$2}'
FMTS="dcr erf mdc mef ppm sr2 srf mos pdf 3fr tiff nrw kdc tif srw x3f mrw pef dng raw raf arw crw orf nef cr2 rw2 jpg"
for w in $FMTS ; do grep -E "th-r-.*\b$w\b" ~/dev/copyparty/copyparty/__main__.py || echo "$w"; done
missing rw2;
FMTS_CPP=3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f,rw2
rm -rf $FS/.hist
time podman run --rm -it -v $FS:/w copyparty/iv -v /w::r --exit=thgen --th-pregen=j
find $FS/.hist/th/ -iname '*.jpg' | wc -l
371 0m5.200s 3s
458 0m5.512s 3s with --th-r-raw=$FMTS_CPP
443 0m3.967s 2s with --th-r-raw=$FMTS_CPP --th-dec=raw
t0=$(date +%s); for f in $FMTS ; do rm -rf $FS/.hist
podman run --rm -it -v $FS:/w copyparty/iv -v /w::r --exit=thgen --th-pregen=j --th-dec=raw --th-r-raw=$f -q >/dev/null 2>&1
printf '%s ' $(find $FS/.hist/th/ -size +0 -iname '*.jpg' | wc -l)
done;t=$(date +%s);echo $((t-t0))
95f 55s --th-dec=ff
397f 51s --th-dec=raw (rawpy)
153f 51s --th-dec=vips (no-magick)
# swithc to persistent for messingaround
podman run --rm -it -v $FS:/w --entrypoint /bin/ash copyparty/iv
apk update
apk upgrade -lai
t0=$(date +%s); for f in $FMTS ; do rm -rf $FS/.hist
podman exec -it d171470581ab python3 -m copyparty -v /w::r --exit=thgen --th-pregen=j --th-dec=vips --th-r-vips=$f -q >/dev/null 2>&1
printf '%s ' $(find $FS/.hist/th/ -size +0 -iname '*.jpg' | wc -l)
done;t=$(date +%s);echo $((t-t0))
# equivalent results
apk add imagemagick; t0=$(date +%s); rm -rf /w/.hist/ ; for f in $FMTS ; do rm -f /*.jpg; n=0; find /w -type f -iname "*.$f" | while IFS= read -r x; do magick "$x" -scale 320x /$n.jpg >/dev/null 2>&1 ; [ -s /$n.jpg ] || rm -f /$n.jpg; n=$((n+1)); done; echo -n "$(ls -1 / | grep -F .jpg | wc -l) "; done; t=$(date +%s); echo $((t-t0))
apk add libraw-tools; t0=$(date +%s); rm -rf /w/.hist/ ; for f in $FMTS ; do rm -f /*.jpg; n=0; find /w -type f -iname "*.$f" | while IFS= read -r x; do [ $(dcraw_emu -h -o 1 -s 0 -Z - "$x" 2>/dev/null | wc -c) -gt 1024 ] && touch /$n.jpg; n=$((n+1)); done; echo -n "$(ls -1 / | grep -F .jpg | wc -l) "; done; t=$(date +%s); echo $((t-t0))
dcr erf mdc mef ppm sr2 srf mos pdf 3fr tiff nrw kdc tif srw x3f mrw pef dng raw raf arw crw orf nef cr2 rw2 jpg
d e m m p s s m p 3 t n k t s x m p d r r a c o n c r j
c r d e p r r o d f i r d i r 3 r e n a a r r r e r w p
r f c f m 2 f s f r f w c f w f w f g w f w w f f 2 2 g
-----------------------------------------------------------------
0 0 0 0 1 1 0 0 0 0 3 0 2 6 0 0 0 6 17 0 0 0 0 0 54 0 0 5 = 95, 55s --th-dec=ff
1 1 0 1 0 1 1 2 0 3 0 4 5 6 7 0 8 17 16 2 30 31 17 45 56 56 87 0 =397, 51s --th-dec=raw ## rawpy
1 1 1 1 0 1 1 2 0 3 0 4 5 6 6 0 9 17 18 24 30 31 34 45 56 57 87 0 =440, 87s dcraw_emu
1 1 1 1 1 1 1 2 0 3 6 4 5 6 6 0 9 17 18 24 30 31 34 45 56 57 87 5 =452, 226s magick-cmd
0 1 0 1 1 0 0 0 2 3 3 0 3 6 0 0 0 0 17 0 0 0 0 0 56 55 0 5 =153, 51s --th-dec=vips ## stock
0 1 0 1 1 1 1 0 2 3 3 4 5 6 6 0 8 0 18 11 30 30 18 39 56 55 87 5 =391, 151s vips + apk add imagemagick imagemagick-raw
0 1 0 1 1 0 0 0 2 3 3 0 3 6 0 0 8 0 17 11 30 0 18 39 56 55 87 5 =346, 128s vips + apk del imagemagick (just imagemagick-raw)
xsel -o | tr ' ' '\n' | awk '!$0{next} {t+=$1} END{print t}'
apk add time
rm -rf /w/.hist; time python3 -m copyparty -v /w::r --exit=thgen --th-pregen=j --th-dec=raw --th-r-raw=$FMTS_CPP ; find /w/.hist/ -iname '*.jpg' -size +0 | wc -l
391f, 0:03.77elapsed 227264maxresident # rawpy+vips with-embedded-thumbs
391f, 0:04.79elapsed 467036maxresident # rawpy+pillow with-embedded-thumbs
434f, 0:29.67elapsed 307724maxresident # dcraw+vips
434f, 0:29.34elapsed 327980maxresident # dcraw+pillow
374f, 1:49.70elapsed 4574768maxresident # vips+imagemagick lol lmao

View file

@ -2,7 +2,7 @@ FROM alpine:3.23
WORKDIR /z WORKDIR /z
ENV ver_hashwasm=4.12.0 \ ENV ver_hashwasm=4.12.0 \
ver_marked=4.3.0 \ ver_marked=4.3.0 \
ver_dompf=3.4.1 \ ver_dompf=3.4.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 \
@ -21,11 +21,14 @@ ENV ver_hashwasm=4.12.0 \
# download; # download;
# the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap # the scp url is regular latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
RUN mkdir -p /z/dist/no-pk \ RUN mkdir -p /z/dist/no-pk \
&& wget https://ocv.me/dev/fonts/orbitron.woff2 -O /z/dist/no-pk/orbitron.woff2 \
&& sha512sum /z/dist/no-pk/orbitron.woff2 | tee /dev/stderr | grep -q b36a69a1b483ca735a3e7d026cdc5e993b8359bbf69b05deadc369fa759f88fe6087bbbbc0be51e62daf034c090df7ee096e5b07884c494aebf56db0e2bc53f7 \
&& wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \ && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \
&& apk add \ && apk add \
bash brotli cmake make g++ git gzip lame npm patch pigz \ bash brotli cmake make g++ git gzip lame npm patch pigz \
python3 python3-dev py3-brotli sox tar unzip wget \ python3 python3-dev py3-pip py3-brotli sox tar unzip wget \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& pip install strip_hints \
&& wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \ && wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
&& wget https://github.com/Ionaru/easy-markdown-editor/archive/$ver_mde.tar.gz -O mde.tgz \ && wget https://github.com/Ionaru/easy-markdown-editor/archive/$ver_mde.tar.gz -O mde.tgz \
&& wget https://github.com/codemirror/codemirror5/archive/$ver_codemirror.tar.gz -O codemirror.tgz \ && wget https://github.com/codemirror/codemirror5/archive/$ver_codemirror.tar.gz -O codemirror.tgz \
@ -34,7 +37,7 @@ RUN mkdir -p /z/dist/no-pk \
&& wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \ && wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \
&& wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \ && wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \
&& wget https://github.com/PrismJS/prism/archive/refs/tags/v$ver_prism.tar.gz -O prism.tgz \ && wget https://github.com/PrismJS/prism/archive/refs/tags/v$ver_prism.tar.gz -O prism.tgz \
&& wget https://files.pythonhosted.org/packages/04/0b/4506cb2e831cea4b0214d3625430e921faaa05a7fb520458c75a2dbd2152/fusepy-3.0.1.tar.gz -O fusepy.tgz \ && wget https://files.pythonhosted.org/packages/91/47/746287c8962274f73ee25edb3840d80899464bfffbe2c435424c2d60a071/mfusepy-3.1.1.tar.gz -O mfusepy.tgz \
&& (mkdir hash-wasm \ && (mkdir hash-wasm \
&& cd hash-wasm \ && cd hash-wasm \
&& unzip ../hash-wasm.zip) \ && unzip ../hash-wasm.zip) \
@ -51,7 +54,7 @@ RUN mkdir -p /z/dist/no-pk \
&& npm i gulp-cli -g ) \ && npm i gulp-cli -g ) \
&& tar --no-same-owner -xf dompurify.tgz \ && tar --no-same-owner -xf dompurify.tgz \
&& tar --no-same-owner -xf prism.tgz \ && tar --no-same-owner -xf prism.tgz \
&& tar --no-same-owner -xf fusepy.tgz \ && tar --no-same-owner -xf mfusepy.tgz \
&& unzip fontawesome.zip \ && unzip fontawesome.zip \
&& tar --no-same-owner -xf zopfli.tgz && tar --no-same-owner -xf zopfli.tgz
@ -148,16 +151,23 @@ RUN cd /z/dist \
&& rmdir no-pk && rmdir no-pk
# build fusepy # build mfusepy -- just sizegolfing for the sfx, mfusepy.py works fine as-is
COPY uncomment.py /z COPY uncomment.py unhint.py /z
RUN mv /z/fusepy-3.0.1/fuse.py /z/dist/f1 \ RUN mv /z/mfusepy-3.1.1/mfusepy.py /z/dist/ \
&& cd /z/dist \ && cd /z/dist \
&& python3 /z/unhint.py \
&& rm -f uh \
&& mv mfusepy.py f1 \
&& python3 /z/uncomment.py f1 \ && python3 /z/uncomment.py f1 \
&& sed -ri '/self.__critical_exception = e/d' f1 \ && sed -ri '/self.__critical_exception/d; /^from (typing|collections.abc) import/d' f1 \
&& awk '/^log =/{s=0} !s; /^from traceback im/{s=1;print"from functools import partial";print"basestring = str"}' <f1 >f2 \ && sed -ri '/^(FieldsEntry|BitFieldsEntry|ReadDirResult) =/d' f1 \
&& awk '/LoggingMixIn:/{exit} --s<0;/self.use_ns = getattr/{s=7}' <f2 >f1 \ && awk "/if TYPE_CHECKING:/{s=1;sub(/TYPE_CHECKING/,"1");print}!s;/else:/{s=0}" <f1 >f2 \
&& awk "/if _machine =/{s=0} /'(mips|ppc|ppc64)'/{s=1} !s" <f1 >f2 \ && awk '/^def _nullable_dummy_function/{print"class Operations:\n pass";exit};1' <f2 >f1 \
&& rm f1 && mv f2 fuse.py && awk '/^# Note that/{s=3} --s<0; /self.use_ns = getattr/{s=16}' <f1 >f2 \
&& awk "/if _machine =/{s=0} /_machine == '(mips|ppc|ppc64)'/{s=1} !s" <f2 >f1 \
&& awk '/else:/{s=0} /elif _system == .NetBSD/{s=1} !s' <f1 >f2 \
&& awk '/^if _system == .NetBSD_False/{s=1;print"if 0:\n pass"} /^elif/{s=0} !s' <f2 >f1 \
&& rm f2 && mv f1 mfusepy.py
# git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\1\2`; s`^(---|\+\+\+) ([ab]/)[^/]+/`\1 \2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch # git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\1\2`; s`^(---|\+\+\+) ([ab]/)[^/]+/`\1 \2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch

View file

@ -5,6 +5,7 @@ vend := $(self)/../../copyparty/web/deps
all: all:
cp -pv ../uncomment.py . cp -pv ../uncomment.py .
cp -pv ../strip_hints/a.py unhint.py
docker build -t build-copyparty-deps . docker build -t build-copyparty-deps .

View file

@ -114,12 +114,14 @@ diff -wNarU2 codemirror-5.65.1-orig/src/input/ContentEditableInput.js codemirror
+ /* + /*
let order = getOrder(line, cm.doc.direction), side = "left" let order = getOrder(line, cm.doc.direction), side = "left"
if (order) { if (order) {
@@ -405,4 +406,5 @@ @@ -405,5 +406,6 @@
side = partPos % 2 ? "right" : "left" side = partPos % 2 ? "right" : "left"
} }
- let result = nodeAndOffsetInLineMap(info.map, pos.ch, side)
+ */ + */
let result = nodeAndOffsetInLineMap(info.map, pos.ch, side) + let result = nodeAndOffsetInLineMap(info.map, pos.ch, "left")
result.offset = result.collapse == "right" ? result.end : result.start result.offset = result.collapse == "right" ? result.end : result.start
return result
diff -wNarU2 codemirror-5.65.1-orig/src/input/movement.js codemirror-5.65.1/src/input/movement.js diff -wNarU2 codemirror-5.65.1-orig/src/input/movement.js codemirror-5.65.1/src/input/movement.js
--- codemirror-5.65.1-orig/src/input/movement.js 2022-01-20 13:06:23.000000000 +0100 --- codemirror-5.65.1-orig/src/input/movement.js 2022-01-20 13:06:23.000000000 +0100
+++ codemirror-5.65.1/src/input/movement.js 2022-02-09 22:50:18.145862052 +0100 +++ codemirror-5.65.1/src/input/movement.js 2022-02-09 22:50:18.145862052 +0100

View file

@ -6,9 +6,10 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
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)"
ENV XDG_CONFIG_HOME=/cfg ENV XDG_CONFIG_HOME=/cfg
ARG ADD_PKG=""
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc ${ADD_PKG} \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure bubblewrap \
py3-jinja2 py3-argon2-cffi py3-pyzmq \ py3-jinja2 py3-argon2-cffi py3-pyzmq \
py3-openssl py3-paramiko py3-pillow py3-openssl py3-paramiko py3-pillow

View file

@ -6,30 +6,29 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
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"
ENV XDG_CONFIG_HOME=/cfg ENV XDG_CONFIG_HOME=/cfg
ARG ADD_PKG=""
COPY i/bin/mtag/install-deps.sh ./ COPY i/bin/mtag/install-deps.sh ./
COPY i/bin/mtag/audio-bpm.py /mtag/ COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/ COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \ RUN apk add -U !pyc ${ADD_PKG} \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure bubblewrap \
py3-jinja2 py3-argon2-cffi py3-pyzmq \ py3-jinja2 py3-argon2-cffi py3-pyzmq \
py3-openssl py3-paramiko py3-pillow \ py3-openssl py3-paramiko py3-pillow \
py3-pip py3-cffi \ py3-pip \
py3-magic \ py3-magic \
vips-jxl vips-poppler vips-magick \ vips-jxl vips-poppler vips-magick \
py3-numpy fftw libsndfile \ py3-numpy fftw libsndfile \
vamp-sdk vamp-sdk-libs keyfinder-cli \ vamp-sdk vamp-sdk-libs keyfinder-cli \
libraw py3-numpy \ libraw-tools \
&& apk add -t .bd \ && apk add -t .bd \
bash wget gcc g++ make cmake patchelf \ bash wget gcc g++ make cmake patchelf \
ffmpeg ffmpeg-dev \ ffmpeg ffmpeg-dev \
python3-dev fftw-dev libsndfile-dev \ python3-dev fftw-dev libsndfile-dev \
py3-wheel py3-numpy-dev libffi-dev \ py3-wheel py3-numpy-dev \
vamp-sdk-dev \ vamp-sdk-dev \
libraw-dev py3-numpy-dev cython \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \ && python3 -m pip install pyvips --no-build-isolation \
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
&& bash install-deps.sh \ && bash install-deps.sh \
&& apk del py3-pip .bd \ && apk del py3-pip .bd \
&& chmod 777 /root \ && chmod 777 /root \

View file

@ -6,8 +6,9 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
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)"
ENV XDG_CONFIG_HOME=/cfg ENV XDG_CONFIG_HOME=/cfg
ARG ADD_PKG=""
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc ${ADD_PKG} \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi \ py3-jinja2 py3-argon2-cffi \
py3-openssl py3-paramiko py3-pillow py3-mutagen py3-openssl py3-paramiko py3-pillow py3-mutagen

View file

@ -6,23 +6,18 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
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)"
ENV XDG_CONFIG_HOME=/cfg ENV XDG_CONFIG_HOME=/cfg
ARG ADD_PKG=""
RUN apk add -U !pyc \ RUN apk add -U !pyc ${ADD_PKG} \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure bubblewrap \
py3-jinja2 py3-argon2-cffi py3-pyzmq \ py3-jinja2 py3-argon2-cffi py3-pyzmq \
py3-openssl py3-paramiko py3-pillow \ py3-openssl py3-paramiko py3-pillow \
py3-pip py3-cffi \ py3-pip \
py3-magic \ py3-magic \
vips-jxl vips-poppler vips-magick \ vips-jxl vips-poppler vips-magick \
libraw py3-numpy \ libraw-tools \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev py3-wheel libffi-dev \
libraw-dev py3-numpy-dev cython \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \ && python3 -m pip install pyvips --no-build-isolation
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
&& apk del py3-pip .bd
COPY i innvikler.sh ./ COPY i innvikler.sh ./
RUN ash innvikler.sh iv RUN ash innvikler.sh iv

View file

@ -6,8 +6,9 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
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"
ENV XDG_CONFIG_HOME=/cfg ENV XDG_CONFIG_HOME=/cfg
ARG ADD_PKG=""
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc ${ADD_PKG} \
py3-jinja2 py3-jinja2
COPY i innvikler.sh ./ COPY i innvikler.sh ./

View file

@ -1,57 +1,32 @@
self := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) self := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
all: .PHONY: i min im ac iv dj clean hclean purge sh
-service docker start
-systemctl start docker
all: ac
i:
rm -rf i rm -rf i
mkdir i mkdir i
tar -cC../.. dist/copyparty-sfx.py bin/mtag | tar -xvCi tar -cC../../dist copyparty-sfx.py | tar -xvCi
tar -cC../.. bin/mtag | tar -xvCi
# # use stock ffmpeg because ./base/Makefile requires podman;
# # beware: https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md
touch i/stock_ffmpeg
min: i
docker build -t copyparty/min:latest -f Dockerfile.min . docker build -t copyparty/min:latest -f Dockerfile.min .
echo 'scale=1;'`docker save copyparty/min:latest | pigz -c | wc -c`/1024/1024 | bc
# docker build -t copyparty/min-pip:latest -f Dockerfile.min.pip .
# echo 'scale=1;'`docker save copyparty/min-pip:latest | pigz -c | wc -c`/1024/1024 | bc
im: i
docker build -t copyparty/im:latest -f Dockerfile.im . docker build -t copyparty/im:latest -f Dockerfile.im .
echo 'scale=1;'`docker save copyparty/im:latest | pigz -c | wc -c`/1024/1024 | bc
docker build -t copyparty/iv:latest -f Dockerfile.iv . ac: i
echo 'scale=1;'`docker save copyparty/iv:latest | pigz -c | wc -c`/1024/1024 | bc docker build -t copyparty/ac:latest -f Dockerfile.ac --build-arg ADD_PKG=ffmpeg .
docker build -t copyparty/ac:latest -f Dockerfile.ac . iv: i
echo 'scale=1;'`docker save copyparty/ac:latest | pigz -c | wc -c`/1024/1024 | bc docker build -t copyparty/iv:latest -f Dockerfile.iv --build-arg ADD_PKG=ffmpeg .
docker build -t copyparty/dj:latest -f Dockerfile.dj . dj: i
echo 'scale=1;'`docker save copyparty/dj:latest | pigz -c | wc -c`/1024/1024 | bc docker build -t copyparty/dj:latest -f Dockerfile.dj --build-arg ADD_PKG=ffmpeg .
docker image ls
min:
rm -rf i
mkdir i
tar -cC../.. dist/copyparty-sfx.py bin/mtag | tar -xvCi
podman build --squash --pull=always -t copyparty/min:latest -f Dockerfile.min .
echo 'scale=1;'`podman save copyparty/min:latest | pigz -c | wc -c`/1024/1024 | bc
push:
docker push copyparty/min
docker push copyparty/im
docker push copyparty/iv
docker push copyparty/ac
docker push copyparty/dj
docker image tag copyparty/min:latest ghcr.io/9001/copyparty-min:latest
docker image tag copyparty/im:latest ghcr.io/9001/copyparty-im:latest
docker image tag copyparty/iv:latest ghcr.io/9001/copyparty-iv:latest
docker image tag copyparty/ac:latest ghcr.io/9001/copyparty-ac:latest
docker image tag copyparty/dj:latest ghcr.io/9001/copyparty-dj:latest
docker push ghcr.io/9001/copyparty-min:latest
docker push ghcr.io/9001/copyparty-im:latest
docker push ghcr.io/9001/copyparty-iv:latest
docker push ghcr.io/9001/copyparty-ac:latest
docker push ghcr.io/9001/copyparty-dj:latest
clean: clean:
-docker kill `docker ps -q` -docker kill `docker ps -q`
@ -66,7 +41,7 @@ hclean:
purge: purge:
-docker kill `docker ps -q` -docker kill `docker ps -q`
-docker rm `docker ps -qa` -docker rm `docker ps -qa`
-docker rmi `docker images -qa` -docker rmi `docker images -qa` -f
sh: sh:
@printf "\n\033[1;31mopening a shell in the most recently created docker image\033[0m\n" @printf "\n\033[1;31mopening a shell in the most recently created docker image\033[0m\n"

View file

@ -54,9 +54,14 @@ with image size after installation and when gzipped
[`ac` is recommended](https://hub.docker.com/r/copyparty/ac) since the additional features available in `iv` and `dj` are rarely useful [`ac` is recommended](https://hub.docker.com/r/copyparty/ac) since the additional features available in `iv` and `dj` are rarely useful
most editions support `x86`, `x86_64`, `armhf`, `aarch64`, `ppc64le`, `s390x` | this architecture | can run these editions |
* `dj` doesn't run on `ppc64le`, `s390x`, `armhf` | ------------------------------ | ----------------------------- |
* `iv` doesn't run on `ppc64le`, `s390x` | `x86` aka `i386` aka `386` | `min`, `im`, `ac`, `iv`, `dj` |
| `x86_64` aka `x64` aka `amd64` | `min`, `im`, `ac`, `iv`, `dj` |
| `AArch64` aka `arm64/v8` | `min`, `im`, `ac`, `iv`, `dj` |
| `arm32` aka `arm/v7` | `min`, `im`, `ac` |
| `ppc64le` aka `PowerPC` | `min`, `im`, `ac` |
| `s390x` aka `IBM mainframe` | `min`, `im`, `ac` |
> NOTE: the following editions are unfinished experiments, and not published anywhere: djd djf djff dju > NOTE: the following editions are unfinished experiments, and not published anywhere: djd djf djff dju

View file

@ -8,7 +8,7 @@ all:
ff: ff:
# legally comfy # legally comfy
/usr/bin/time ./build-no265.sh img /usr/bin/time ./build-no265.sh pull img
zlib: zlib:

View file

@ -1,9 +1,13 @@
#!/bin/ash #!/bin/ash
set -e set -e
AVER=3.24
[ $1 = 1 ] && hub=1 [ $1 = 1 ] && hub=1
uname -a uname -a
apk update
apk upgrade -al
apk add alpine-sdk doas wget apk add alpine-sdk doas wget
echo permit nopass root > /etc/doas.d/u.conf echo permit nopass root > /etc/doas.d/u.conf
cp -pv /root/.abuild/*.pub /etc/apk/keys/ || abuild-keygen -ina cp -pv /root/.abuild/*.pub /etc/apk/keys/ || abuild-keygen -ina
@ -13,7 +17,7 @@ cp -pv /root/.abuild/*.pub /etc/apk/keys/ || abuild-keygen -ina
mkdir /ffmpeg mkdir /ffmpeg
cd /ffmpeg cd /ffmpeg
base=https://github.com/alpinelinux/aports/raw/refs/heads/3.23-stable/community/ffmpeg/ base=https://github.com/alpinelinux/aports/raw/refs/heads/$AVER-stable/community/ffmpeg/
wget ${base}APKBUILD wget ${base}APKBUILD
awk <APKBUILD -vb="$base" '/"/{o=0}/^source=/{o=1;next}o{print b $1}' | wget -i- awk <APKBUILD -vb="$base" '/"/{o=0}/^source=/{o=1;next}o{print b $1}' | wget -i-
cp -pv APKBUILD /root/ cp -pv APKBUILD /root/
@ -29,16 +33,23 @@ prepare() {
default_prepare default_prepare
tar -cC/opt/patch/ffmpeg . | tar -x tar -cC/opt/patch/ffmpeg . | tar -x
patch -p1 <aac-lc-only.patch patch -p1 <aac-lc-only.patch
awk >t <libavcodec/aac/aacdec_tab.c '/^[^ \t]/{o=0} /^(static|const).*( sbr_|_hcod)/{o=1} !o{print;next} {gsub(/\{ *-?[0-9]+, *-?[0-9]+ *\}/, "{ 1, 1 }")}1'
mv t libavcodec/aac/aacdec_tab.c
# invent the missing disable-option for this crap
sed -ri 's/(^v4l2_m2m_deps=")/\1videotoolbox /' configure
} }
EOF EOF
## ##
## shrink-ray ## shrink-ray
sed -ri 's/--enable-lib(bluray|placebo|rav1e|shaderc)/--disable-lib\1/; s/--enable-(vdpau)/--disable-\1/; s/\b(rav1e|shaderc)-dev//; s/\blib(bluray|placebo|vdpau|xfixes)-dev\b//' APKBUILD sed -ri 's/--enable-lib(bluray|dvdnav|dvdread|placebo|rav1e|shaderc)/--disable-lib\1/; s/--enable-(vdpau)/--disable-\1/; s/\b(rav1e|shaderc)-dev//; s/\blib(bluray|placebo|vdpau|xfixes)-dev\b//' APKBUILD
# `- rm placebo+shaderc to drop spirv-tools (1.7 MiB apk) # `- rm placebo+shaderc to drop spirv-tools (1.7 MiB apk)
sed -ri 's/--enable-libxcb/--disable-libxcb --disable-indev=xcbgrab --disable-ffplay --disable-encoder=opus /' APKBUILD sed -ri 's/--enable-libxcb/--disable-libxcb --disable-indev=xcbgrab --disable-ffplay --disable-encoder=opus --disable-decoder=metasound --disable-decoder=twinvq/' APKBUILD
# `- metasound+twinvq = +450 KiB apk
sed -ri 's/\bffplay$//; s/\bsdl2-dev\b//' APKBUILD sed -ri 's/\bffplay$//; s/\bsdl2-dev\b//' APKBUILD
## ##
@ -48,10 +59,12 @@ sed -ri 's/\bffplay$//; s/\bsdl2-dev\b//' APKBUILD
sed -ri 's/--enable-(ladspa|lv2|vaapi|vulkan)/--disable-\1/' APKBUILD sed -ri 's/--enable-(ladspa|lv2|vaapi|vulkan)/--disable-\1/' APKBUILD
sed -ri 's/--enable-lib(aom|ass|drm|fontconfig|freetype|fribidi|harfbuzz|pulse|rist|srt|ssh|v4l2|vidstab|x264|xvid|zimg|vpl)/--disable-lib\1/' APKBUILD sed -ri 's/--enable-lib(aom|ass|drm|fontconfig|freetype|fribidi|harfbuzz|pulse|rist|srt|ssh|v4l2|vidstab|x264|xvid|zimg|vpl)/--disable-lib\1/' APKBUILD
sed -ri 's/\b(v4l-utils|libvpx)-dev\b//' APKBUILD # (try to) drop v4l2_m2m, and use builtin vp8/vp9 instead of libvpx for decode sed -ri 's/\b(v4l-utils|libvpx)-dev\b//' APKBUILD # (try to) drop v4l2_m2m, and use builtin vp8/vp9 instead of libvpx for decode
sed -ri 's/(--disable-vulkan)/\1 --disable-devices --disable-hwaccels --disable-encoders --enable-encoder=flac --enable-encoder=libjxl --enable-encoder=libmp3lame --enable-encoder=libopus --enable-encoder=libwebp --enable-encoder=mjpeg --enable-encoder=pcm_s16le --enable-encoder=pcm_s16le_planar --enable-encoder=png --enable-encoder=rawvideo --enable-encoder=vnull --enable-encoder=wrapped_avframe --disable-muxers --enable-muxer=aiff --enable-muxer=apng --enable-muxer=caf --enable-muxer=ffmetadata --enable-muxer=fifo --enable-muxer=flac --enable-muxer=image2 --enable-muxer=image2pipe --enable-muxer=matroska --enable-muxer=matroska_audio --enable-muxer=mjpeg --enable-muxer=mp3 --enable-muxer=null --enable-muxer=opus --enable-muxer=pcm_s16le --enable-muxer=wav --enable-muxer=webm --enable-muxer=webp --enable-muxer=yuv4mpegpipe --disable-filters --enable-filter=anoisesrc --enable-filter=asplit --enable-filter=amerge --enable-filter=amix --enable-filter=aresample --enable-filter=crop --enable-filter=showspectrumpic --enable-filter=showwavespic --enable-filter=convolution --enable-filter=volume --enable-filter=compand --enable-filter=setsar --enable-filter=scale --disable-decoder=av1 --disable-hwaccel=v4l2_m2m --disable-decoder=h263_v4l2m2m --disable-decoder=h264_v4l2m2m --disable-decoder=mpeg1_v4l2m2m --disable-decoder=mpeg2_v4l2m2m --disable-decoder=mpeg4_v4l2m2m --disable-decoder=vc1_v4l2m2m --disable-decoder=vp8_v4l2m2m --disable-decoder=vp9_v4l2m2m --disable-decoder=subrip --disable-decoder=srt --disable-decoder=pgssub --disable-decoder=cc_dec --disable-decoder=dvdsub --disable-decoder=dvbsub --disable-decoder=ssa --disable-decoder=ass --disable-decoder=opus /' APKBUILD sed -ri 's/(--disable-vulkan)/\1 --disable-devices --disable-hwaccels --disable-encoders --enable-encoder=flac --enable-encoder=libjxl --enable-encoder=libmp3lame --enable-encoder=libopus --enable-encoder=libwebp --enable-encoder=mjpeg --enable-encoder=pcm_f32le --enable-encoder=pcm_s16le --enable-encoder=pcm_s16le_planar --enable-encoder=png --enable-encoder=rawvideo --enable-encoder=vnull --enable-encoder=wrapped_avframe --disable-muxers --enable-muxer=aiff --enable-muxer=apng --enable-muxer=caf --enable-muxer=ffmetadata --enable-muxer=fifo --enable-muxer=flac --enable-muxer=image2 --enable-muxer=image2pipe --enable-muxer=matroska --enable-muxer=matroska_audio --enable-muxer=mjpeg --enable-muxer=mp3 --enable-muxer=null --enable-muxer=opus --enable-muxer=pcm_f32le --enable-muxer=pcm_s16le --enable-muxer=wav --enable-muxer=webm --enable-muxer=webp --enable-muxer=yuv4mpegpipe --disable-filters --enable-filter=anoisesrc --enable-filter=asplit --enable-filter=amerge --enable-filter=amix --enable-filter=aresample --enable-filter=crop --enable-filter=showspectrumpic --enable-filter=showwavespic --enable-filter=convolution --enable-filter=volume --enable-filter=compand --enable-filter=setsar --enable-filter=scale --disable-decoder=av1 --disable-hwaccel=v4l2_m2m --disable-decoder=h263_v4l2m2m --disable-decoder=h264_v4l2m2m --disable-decoder=mpeg1_v4l2m2m --disable-decoder=mpeg2_v4l2m2m --disable-decoder=mpeg4_v4l2m2m --disable-decoder=vc1_v4l2m2m --disable-decoder=vp8_v4l2m2m --disable-decoder=vp9_v4l2m2m --disable-decoder=subrip --disable-decoder=srt --disable-decoder=pgssub --disable-decoder=cc_dec --disable-decoder=dvdsub --disable-decoder=dvbsub --disable-decoder=ssa --disable-decoder=ass --disable-decoder=opus /' APKBUILD
# `- s/av1/libdav1d/; s/libvorbis/vorbis/; s/opus/libopus/; libvorbis and mpg123 gets pulled in by openmpt # `- s/av1/libdav1d/; s/libvorbis/vorbis/; s/opus/libopus/; libvorbis and mpg123 gets pulled in by openmpt
} }
[ $1 -gt 1 ] && sed -ri 's/(--disable-libxcb )/\1--disable-doc --disable-htmlpages --disable-manpages --disable-podpages --disable-txtpages /' APKBUILD
p=/root/packages/$(abuild -A) p=/root/packages/$(abuild -A)
rm -rf $p rm -rf $p
abuild -FrcK abuild -FrcK
@ -59,6 +72,7 @@ abuild -FrcK
mkdir $p/ex mkdir $p/ex
mv $p/ffmpeg-d* $p/ex # dbg,dev,doc mv $p/ffmpeg-d* $p/ex # dbg,dev,doc
cp -pv src/ffmpeg-*/ffbuild/config.log $p/ cp -pv src/ffmpeg-*/ffbuild/config.log $p/
#tar -cz src > $p/.tar
[ $hub ] && rm -rf $p.hub && mv $p $p.hub [ $hub ] && rm -rf $p.hub && mv $p $p.hub

View file

@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
AVER=3.24
[ $(id -u) -eq 0 ] && { [ $(id -u) -eq 0 ] && {
echo dont root echo dont root
exit 1 exit 1
@ -34,8 +36,8 @@ wt() {
} }
[ $pull ] && { [ $pull ] && {
for a in $sarchs; do # arm/v6 for a in $sarchs; do
podman pull --arch=$a alpine:latest podman pull --arch=$a alpine:$AVER
done done
podman images --format "{{.ID}} {{.History}}" | podman images --format "{{.ID}} {{.History}}" |
@ -62,19 +64,20 @@ wt() {
# kill abandoned builders # kill abandoned builders
ps aux | awk '/bin\/qemu-[^-]+-static/{print$2}' | xargs -r kill -9 ps aux | awk '/bin\/qemu-[^-]+-static/{print$2}' | xargs -r kill -9
n=0; set -x n=0; set -xo pipefail
for a in $archs; do for a in $archs; do
n=$((n+1)); wt "$n/$a" n=$((n+1)); wt "$n/$a"
#[ $n -le 3 ] || continue #[ $n -le 3 ] || continue
touch b/t.$a.1.$(date +%s) touch b/t.$n.$a.1.$(date +%s)
tar -c arbeidspakke.sh patch/ffmpeg | tar -c arbeidspakke.sh patch/ffmpeg |
time nice podman run \ time nice podman run \
--rm -i --pull=never -v "$self/b:/root:z" localhost/alpine-$a \ --rm -i --pull=never -v "$self/b:/root:z" localhost/alpine-$a \
/bin/ash -c "cd /opt;tar -x;/bin/ash ./arbeidspakke.sh $n $a" 2>&1 | /bin/ash -c "cd /opt;tar -x;/bin/ash ./arbeidspakke.sh $n $a" 2>&1 |
tee b/log.$a awk '{getline a<"/proc/uptime";close("/proc/uptime");sub(/ .*/,"",a);printf"%.2f %s\n",a-p,$0;p=a}' |
tee b/log.$n.$a
touch b/t.$a.2.$(date +%s) touch b/t.$n.$a.2.$(date +%s)
done done
wt -;wt "" wt -;wt ""
} }
@ -89,11 +92,23 @@ echo ok
# 50m01.04 s390x # 50m01.04 s390x
# golflympics # golflympics
# 4:09 x86_64-hub # 3:48 x86_64-hub
# 2:57 x86_64 # 2:46 x86_64
# 2:54 x86 # 2:24 x86
# 31:13 aarch64 # 28:50 aarch64
# 22:38 armv7 # 21:34 armv7
# 32:17 s390x # 31:13 s390x
# 24:27 ppc64le # 22:50 ppc64le
# 2:00:35 summa summarum # 1:53:25 summa summarum
# for a in version muxers demuxers devices decoders encoders filters pix_fmts layouts sample_fmts ; do ffmpeg -hide_banner -$a; done | nc 192.168.123.1 4321
# v=3.24-stable
# echo -n https://dl-cdn.alpinelinux.org/v${v%-*}/releases/x86_64/ >aver
# curl -s $(cat aver)latest-releases.yaml | awk '/alpine-minirootfs-3.*gz$/{print$2;exit}' | grep ... >> aver
# podman import $(cat aver) a324
# f(){ p=/sys/fs;for w in cgroup user.slice user-1000.slice user@1000.service user.slice ;do p="$p/$w";echo $1>"$p/cgroup.subtree_control";done;}
# f +cpuset
# time nice podman run --cpuset-cpus=1 \
# grep -E '^[^0].*' -B2 -A1 log.1.amd64 # offbyone, whatever, just eyeball it
# f -cpuset

View file

@ -1,16 +1,19 @@
#!/bin/bash #!/bin/bash
set -e set -e
v=3.23 AVER=3.24
v=$AVER-stable
#v=master
mkdir -p cver mkdir -p cver
rm -rf cver2 rm -rf cver2
mkdir cver2 mkdir cver2
cd cver2 cd cver2
curl \ curl \
-Lo1 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v-stable/main/musl/APKBUILD \ -Lo1 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v/main/musl/APKBUILD \
-Lo2 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v-stable/main/python3/APKBUILD \ -Lo2 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v/main/python3/APKBUILD \
-Lo3 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v-stable/community/ffmpeg/APKBUILD \ -Lo3 https://raw.githubusercontent.com/alpinelinux/aports/refs/heads/$v/community/ffmpeg/APKBUILD \
; ;
zlib= ff= zlib= ff=

View file

@ -15,7 +15,7 @@ ised() {
} }
# use custom ffmpeg if relevant # use custom ffmpeg if relevant
echo $1 | grep -qE 'ac|iv|dj' && ( echo $1 | grep -qE 'ac|iv|dj' && [ ! -e "/z/stock_ffmpeg" ] && (
cp -pv /z/packages/*.pub /etc/apk/keys/ cp -pv /z/packages/*.pub /etc/apk/keys/
cd /z/packages/$(cat /etc/apk/arch) cd /z/packages/$(cat /etc/apk/arch)
apk add ./ffmpeg-*.apk apk add ./ffmpeg-*.apk

View file

@ -16,7 +16,7 @@ imgs="dj iv min im ac"
dhub_order="iv dj min im ac" dhub_order="iv dj min im ac"
ghcr_order="ac im min dj iv" ghcr_order="ac im min dj iv"
ngs=( ngs=(
iv-{ppc64le,s390x} iv-{ppc64le,s390x,arm}
dj-{ppc64le,s390x,arm} dj-{ppc64le,s390x,arm}
) )

View file

@ -511,6 +511,7 @@ unhelpg() {
[ $no_hl ] && [ $no_hl ] &&
rm -rf copyparty/web/deps/prism* rm -rf copyparty/web/deps/prism*
rm -f copyparty/web/deps/orbitron.woff2 # todo:uiv15
[ $no_fnt ] && { [ $no_fnt ] && {
rm -f copyparty/web/deps/scp.woff2 rm -f copyparty/web/deps/scp.woff2
f=copyparty/web/ui.css f=copyparty/web/ui.css

View file

@ -23,12 +23,12 @@ ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de
# win10 # win10
f4b4e330995ebe96c0bd06e16e5b26062ece9473f06d369775aa68eab261dedcf32dfdd159acaa227502bbf9fa2cd8bbe57cddb89fc6f2196fef7a9ed5a80ae9 Git-2.51.0-64-bit.exe f4b4e330995ebe96c0bd06e16e5b26062ece9473f06d369775aa68eab261dedcf32dfdd159acaa227502bbf9fa2cd8bbe57cddb89fc6f2196fef7a9ed5a80ae9 Git-2.51.0-64-bit.exe
0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso 0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso
16cc0c58b5df6c7040893089f3eb29c074aed61d76dae6cd628d8a89a05f6223ac5d7f3f709a12417c147594a87a94cc808d1e04a6f1e407cc41f7c9f47790d1 virtio-win-0.1.285.iso 4f13070cc9241fa342deab4ebfac360565030580ff77b6e5f1951a64627621e5da4abfd30e1e46ca8bae2bb7dd4ff98141aff424142c9629a5876a61283962e5 virtio-win-0.1.285.iso
9a7f40edc6f9209a2acd23793f3cbd6213c94f36064048cb8bf6eb04f1bdb2c2fe991cb09f77fe8b13e5cd85c618ef23573e79813b2fef899ab2f290cd129779 jinja2-3.1.6-py3-none-any.whl 9a7f40edc6f9209a2acd23793f3cbd6213c94f36064048cb8bf6eb04f1bdb2c2fe991cb09f77fe8b13e5cd85c618ef23573e79813b2fef899ab2f290cd129779 jinja2-3.1.6-py3-none-any.whl
00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3 MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl 00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3 MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl 8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae packaging-25.0-py3-none-any.whl a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae packaging-25.0-py3-none-any.whl
efc712162da7fb005c8869a7612d2f4983d2d073ec79e16a58e7bf1fcd01c88b1cc26656f0893c68edd2294be7c3990db2f6bd77e7e3f2613539d57994b6a033 pillow-12.1.1-cp313-cp313-win_amd64.whl 5459cfe12d953ed37c481a992e3509536b7997fbd1bb77158d3465d86d3d57af9a16fd4d695374fe6ed30cbb12ac90a2de3000dd92897ddf8bdcfc3e3de831bd pillow-12.2.0-cp313-cp313-win_amd64.whl
b9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef pyinstaller-6.15.0-py3-none-win_amd64.whl b9b98714dfca6fa80b0b3f222965724d63be9c54d19435d1fe768e07016913d6db8d6e043fcb185b55a9bd6fe370a80cf961814fc096046a5f4640d99ed575ef pyinstaller-6.15.0-py3-none-win_amd64.whl
cad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0 pyinstaller_hooks_contrib-2025.8-py3-none-any.whl cad0f7cf39de691813b1d4abc7d33f8bda99a87d9c5886039b814752e8690364150da26fb61b3e28d5698ff57a90e6dcd619ed2b64b04f72b5aadb75e201bdb0 pyinstaller_hooks_contrib-2025.8-py3-none-any.whl
368ea2da3e3bfe765a37c62227e84774853aaabce6954475fa45c873e5547cb5346ca03a0f6a0789af369285bb3464881fed0275a19066913d9d396d5d9b9947 python-3.13.13-amd64.exe 368ea2da3e3bfe765a37c62227e84774853aaabce6954475fa45c873e5547cb5346ca03a0f6a0789af369285bb3464881fed0275a19066913d9d396d5d9b9947 python-3.13.13-amd64.exe

View file

@ -39,7 +39,7 @@ fns=(
MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
mutagen-1.47.0-py3-none-any.whl mutagen-1.47.0-py3-none-any.whl
packaging-25.0-py3-none-any.whl packaging-25.0-py3-none-any.whl
pillow-12.1.1-cp313-cp313-win_amd64.whl pillow-12.2.0-cp313-cp313-win_amd64.whl
pyinstaller-6.15.0-py3-none-win_amd64.whl pyinstaller-6.15.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2025.8-py3-none-any.whl pyinstaller_hooks_contrib-2025.8-py3-none-any.whl
python-3.13.13-amd64.exe python-3.13.13-amd64.exe

View file

@ -80,7 +80,7 @@ copyparty/web/deps/__init__.py,
copyparty/web/deps/busy.mp3, copyparty/web/deps/busy.mp3,
copyparty/web/deps/easymde.css, copyparty/web/deps/easymde.css,
copyparty/web/deps/easymde.js, copyparty/web/deps/easymde.js,
copyparty/web/deps/fuse.py, copyparty/web/deps/mfusepy.py,
copyparty/web/deps/marked.js, copyparty/web/deps/marked.js,
copyparty/web/deps/mini-fa.css, copyparty/web/deps/mini-fa.css,
copyparty/web/deps/mini-fa.woff, copyparty/web/deps/mini-fa.woff,

View file

@ -38,7 +38,10 @@ class TestVFS(unittest.TestCase):
unpacked.append(list(sorted(getattr(axs, k)))) unpacked.append(list(sorted(getattr(axs, k))))
pad = len(unpacked) - len(expected) pad = len(unpacked) - len(expected)
self.assertEqual(unpacked, expected + [[]] * pad) want = expected + [[]] * pad
if want[0] and not want[4]:
want[4] = want[0]
self.assertEqual(unpacked, want)
def assertAxsAt(self, au, vp, expected): def assertAxsAt(self, au, vp, expected):
vn = self.nav(au, vp) vn = self.nav(au, vp)

View file

@ -188,8 +188,8 @@ class TestVFS(unittest.TestCase):
self.assertAxs(n.axs.uread, ["*", "k"]) self.assertAxs(n.axs.uread, ["*", "k"])
self.assertAxs(n.axs.uwrite, []) self.assertAxs(n.axs.uwrite, [])
perm_na = (False, False, False, False, False, False, False, False, False) perm_na = (False, False, False, False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False, False, False, False) perm_rw = (True, True, False, False, True, False, False, False, False)
perm_ro = (True, False, False, False, False, False, False, False, False) perm_ro = (True, False, False, False, True, False, False, False, False)
self.assertEqual(vfs.can_access("/", "*"), perm_na) self.assertEqual(vfs.can_access("/", "*"), perm_na)
self.assertEqual(vfs.can_access("/", "k"), perm_rw) self.assertEqual(vfs.can_access("/", "k"), perm_rw)
self.assertEqual(vfs.can_access("/a", "*"), perm_ro) self.assertEqual(vfs.can_access("/a", "*"), perm_ro)