Compare commits

...

48 commits

Author SHA1 Message Date
ed fbcb773307 failed the git skillcheck 2026-08-06 22:10:18 +00:00
ed 3eb7b46d83 optimize 2026-08-06 21:08:34 +00:00
h3 4f20738b71 haproxy: add X-Forwarded-Proto to example (codeberg#6)
Reviewed-on: https://codeberg.org/9001/copyparty/pulls/6
2026-08-05 01:52:58 +02:00
ed ef75220faf update pkgs to 1.20.20 2026-08-03 22:36:36 +00:00
ed a3acbfcd5b v1.20.20 2026-08-03 22:32:26 +00:00
Danila Kamaev fdf52a8c4e
wopi: fix onlyoffice opening docs in readonly mode (#1585) 2026-08-03 22:02:18 +00:00
ed b9405a3e25 mute no-rss-warning for shares 2026-08-03 21:57:22 +00:00
ed 6f924829ee detect broken csp nonce 2026-08-03 21:56:45 +00:00
Danila Kamaev 24a4090fc7
wopi: add wopi-urls option (#1580);
map different copyparty hosts to different WOPI client addresses
2026-08-03 20:30:38 +00:00
ed c0c638c917 up2k: re-hs if unknown-wark or chunk-exists;
previous behavior:

* when a chunk already existed on the server, it would
   unstitch and continue retrying each individual chunk

* upon unknown-wark (file already completed), it would
   upload the remaining scheduled chunks before bailing

the unknown-wark behavior was accidental bullshit

the chunk-exists behavior was intentional, assumed optimal for
preserving tcp window-scaling, just not when running behind a
bufferbloating reverseproxy which may disconnect the client on
a timeout before the response is delivered, and "some clients"
still follow rfc2616-8.2.4 (retries the POST, wtf...)
wasting bandwidth + maybe further worsening conditions

"some clients" is specifically firefox with the devtools not open

another firefox joke is that it doesn't read the server-response
if the server does not drain the request-body; we end up in
xhr.onerror with no idea why, must assume the worst

new behavior:

in both scenarios, drop all scheduled chunks to be uploaded and
redo handshake, to as far as possible avoid retransmissions;
firefox will still eagerly retry 4 times but whatever

u2c was/is fine; nothing to be done

depending on network conditions and physical distance, this MAY
result in lower total speed, but the average case is likely a
net-positive, wasting less bandwidth on retransmissions,
in exchange for resetting tcp window-scaling
2026-08-03 20:08:19 +00:00
ed 2c6497b644 add hotkey F4 2026-08-03 18:55:24 +00:00
ed 2c4033831f js: while(apop) 2026-08-02 23:02:44 +00:00
ed 2880117265 fix ^a in search-results; closes #1581 2026-08-02 22:41:57 +00:00
yuki 63c61b6729
nixos: fix version checker (#1582)
Nixpkgs patches many libraries to read certificate bundle mentioned in
NIX_SSL_CERT_FILE, and /nix/store is already present in the sandbox.

Related to https://github.com/NixOS/nixpkgs/issues/409848
2026-08-02 21:48:24 +00:00
ed 5c157be955 show subvolume size in listings;
* descends recursively to all nested volumes
* values are static, update on volume-rescan
* closes #634 #1033 #1390

Co-authored-by: Victor Phillips <victor.m.phillips@protonmail.com>
2026-08-01 22:23:58 +00:00
Wuerfel21 9f5f9d8884
th: .kra (krita) + .ora (openraster) (#1577) 2026-07-30 18:39:31 +00:00
ed 4003a47723 multipart boundary max-len 2026-07-30 18:22:45 +00:00
ed 398fcf1d18 update pkgs to 1.20.19 2026-07-27 22:08:01 +00:00
ed ac4be555f6 v1.20.19 2026-07-27 22:02:50 +00:00
ed 75c7502c34 mkdir: translit illegals instead of failing 2026-07-27 21:25:21 +00:00
ed 9912a95166 ftp: logging + fix xbu log 2026-07-27 19:06:25 +00:00
ed b331bb1c8c ftp: fix GHSA-phv8-wgjp-g4p9 (stou) 2026-07-27 18:50:44 +00:00
ed c122e103bd hooks: xar/xbr: old/new ap 2026-07-27 18:45:35 +00:00
ed 6226858b89 windows: fix fsnt detection 2026-07-27 17:35:45 +00:00
ed cea97ac6fd windows: restrict tool paths;
the warning in commit bb40804f2d is still true, but
this makes copyparty slightly safer in environments
where hostile binaries may appear in CWD
2026-07-27 17:34:52 +00:00
ed bc45299b86 windows: optimize sparse file creation;
previously shelled out to system32/fsutil.exe; expensive and
pointless since all it does is call DeviceIoControl (ioctl)

this is not a security fix; just like most software on windows,
copyparty is still vulnerable to DLL hijacking, so this change
makes little-to-no difference security-wise; commit bb40804f2d

plus, the default config (sharing current folder read-write
without auth) was meant to be obviously insecure, intended for
quick shares on a small LAN rather than untrusted networks
2026-07-27 17:20:25 +00:00
ed bb40804f2d windows: add DLL-hijacking warning;
famously, windows prefers cwd when resolving a binary to execute,
rather than a predefined PATH like linux / unix / every other OS

even worse, it also does this for DLLs, and mitigating *that*
is infeasible because it depends on variables we dont control

print a warning in case someone is unaware of this
2026-07-27 17:15:00 +00:00
ed c70dc7ac56 android: fix cross-fs mv; closes #1563 2026-07-26 21:19:33 +00:00
ed b6abc33fb3 lo: rlo: more spaghetti 2026-07-26 16:49:05 +00:00
ed 537a99df52 bump session-cookie from 20 to 24 ch 2026-07-26 16:47:17 +00:00
ed cdb474c8ec fix slow boot when a volume root has many files 2026-07-26 16:45:49 +00:00
ed 0f2040c383 ceil by integer math 2026-07-26 15:23:33 +00:00
ed e49f6cca17 py2: future division 2026-07-26 15:19:58 +00:00
ed 10db4236bf wopi: new-file, j2-template 2026-07-24 15:34:56 +00:00
Brandon Doornbos d57bb0c701
add wopi (#1495)
basic wopi integration

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-07-24 15:28:05 +00:00
Nia fface524c3
nix: use ffmpeg-headless (#1530) 2026-07-22 17:16:02 +02:00
ed bae77b9078 hooks: xau: fix argv (ap, not wark) 2026-07-21 18:40:47 +00:00
ed b2e4b1c46c shares: dont suggest write for fileshares;
the ui would still show the option to grant write-permission
when creating a fileshare; pointless since an existing file
can't be overwritten without the delete-permission

while this is obvious and nobody would intentionally create
a fileshare with write, avoid any confusion by hiding it
2026-07-18 20:36:41 +00:00
ed 4543ab31d3 cgen cleanup 2026-07-17 22:20:56 +00:00
ed 856fada3c7 th-covers volflag; no disables folderthumbs 2026-07-17 22:18:17 +00:00
stackxp aa86235360
hooks: xiu: fix fork (#1558) 2026-07-17 18:33:06 +00:00
ed 6a9437b77c shares: fix md-viewer 2026-07-16 20:32:35 +00:00
ed 5952401815 th: video: prefer embedded cover/pic 2026-07-15 22:00:33 +00:00
ed ed0be42a3b th: demote vips (#1556);
use libvips as last resort due to risk of OOM; example:

libvips defers to imagemagick for animated avif, and imagemagick
probably does an identify, iterating through each frame, itself
consuming 100+ MiB RAM, which libvips amplifies to 7+ GiB

similar behavior is observed with `identify a.mkv` in a shell
2026-07-15 21:59:45 +00:00
ed daf144af61 up2k: fix dragdrop datatransfer type detection;
`text/uri-list` observed before `Files` once on wayland dolphin

Co-authored-by: Til Schmitter <til.schmitter@gmail.com>
2026-07-15 21:21:32 +00:00
ed a7c99094dd add --srch-nfkc (closes #1555);
option to unicode-normalize filepaths during search; useful for:
* macos servers with cjk filenames
* cjk stuff originating from a macos device
2026-07-15 20:09:11 +00:00
ed daee3e8500 docs: config include (#1554) 2026-07-12 18:47:15 +00:00
ed 96bafb8f4a update pkgs to 1.20.18 2026-07-09 22:59:43 +00:00
102 changed files with 874 additions and 237 deletions

1
.gitattributes vendored
View file

@ -4,4 +4,5 @@
*.png binary
*.gif binary
*.ico binary
*.gz binary

View file

@ -76,6 +76,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI
* [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
* [wopi server](#wopi-server) - edit office documents in the web-ui
* [browser ux](#browser-ux) - tweaking the ui
* [opengraph](#opengraph) - discord and social-media embeds
* [file deduplication](#file-deduplication) - enable symlink-based upload deduplication
@ -136,6 +137,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [client-side](#client-side) - when uploading files
* [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements
* [gotchas](#gotchas) - behavior that might be unexpected
* [csp nonce](#csp-nonce) - unauthorized javascript is blocked
* [cors](#cors) - cross-site request config
* [filekeys](#filekeys) - prevent filename bruteforcing
* [dirkeys](#dirkeys) - share specific folders in a volume
@ -773,7 +775,7 @@ press `g` or `田` to toggle grid-view instead of the file listing and `t` togg
![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png)
it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
* Pillow is 3x faster (and safer) than FFmpeg
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
* for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies)
@ -782,6 +784,7 @@ audio files are converted into spectrograms using FFmpeg unless you `--no-athumb
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
* the order is significant, so if both `cover.png` and `folder.jpg` exist in a folder, it will pick the first matching `--th-covers` entry (`folder.jpg`)
* and, if you enable [file indexing](#file-indexing), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all)
* disable folderthumbs with `--th-covers no`
enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
@ -1337,7 +1340,8 @@ for the above example to work, add the commandline argument `-e2ts` to also scan
# server config
using arguments or config files, or a mix of both:
* config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf)
* using a config-file (`-c some.conf`) is best, easier to read/maintain; see [./docs/example.conf](docs/example.conf)
* for complex and intricate setups, see how [./docs/example2.conf](docs/example2.conf) includes additional config-files/folders with `% copyparty.d` which loads [./docs/copyparty.d/some.conf](docs/copyparty.d/some.conf)
* `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting
* or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume
* changes to the `[global]` config section requires a restart to take effect
@ -1629,6 +1633,15 @@ authenticate with one of the following:
* username `$password`, password `k`
## wopi server
edit office documents in the web-ui
needs a wopi client to work, and the only supported wopi client is a selfhosted collabora online server; eventually onlyoffice will probably also be supported: [issue #1574](https://github.com/9001/copyparty/issues/1574)
no further documentation or examples yet, [issue #1575](https://github.com/9001/copyparty/issues/1575)
## browser ux
tweaking the ui
@ -2575,6 +2588,7 @@ buggy feature? rip it out by setting any of the following environment variables
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
| `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads |
| `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |
| `PRTY_NO_UNIDATA` | disable loading the unicodedata c-extension; saves 400k ram, breaks `--srch-nfkc` |
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
@ -2650,8 +2664,6 @@ requires a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of nix
some recommended dependencies are enabled by default; [override the package](https://github.com/9001/copyparty/blob/hovudstraum/contrib/package/nix/copyparty/default.nix#L3-L22) if you want to add/remove some features/deps
`ffmpeg-full` was chosen over `ffmpeg-headless` mainly because we need `withWebp` (and `withOpenmpt` is also nice) and being able to use a cached build felt more important than optimizing for size at the time -- PRs welcome if you disagree 👍
## nixos module
@ -3096,6 +3108,23 @@ behavior that might be unexpected
* or eliminate the problem entirely by only giving write-access to trustworthy people :^)
## csp nonce
unauthorized javascript is blocked by means of the default [content-security-policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) which enforces "script nonces", to help prevent XSS and similar
usually this Just Works and is correctly configured by default; you can stop reading this section and move on, nothing to see here
but if you are the unlucky 0.1% with a hosting-setup funky enough for this to not work as intended, then you will also be seeing this message: `Looks like javascript is broken; see "csp nonce" in the readme`
there is two possible reasons why this might happen:
1. your browser is simply too old to understand the javascript, which is not likely, because just about every browser is supported, even internet explorer 11
2. something in your hosting-setup is tampering with the responses from copyparty and destroying this security feature; the culprit is probably a misconfigured reverseproxy, a CDN, a WAF, or some other mitm, but a popular offender is **"cloudflare rocket loader"**
you can try to **temporarily** set global-options `--csp-ui no` and `--csp-dl no` and restart copyparty to check whether it is due to reason #2. Remember to refresh the webbrowser with CTRL-SHIFT-R to check properly. If that makes the problem go away, then turn them back off (**never** keep them enabled), restart copyparty, open the `Network` tab in the webbrowser, and refresh the website; you should see the `content-security-policy` header including a nonce, for example `nonce-BxcJbvdu1z3S/ycPBjVWuA==` which should also appear inside the HTML of that response like this: `<script nonce="BxcJbvdu1z3S/ycPBjVWuA==">` -- if it doesn't then that's the problem; something in your hosting stack is dangerously misconfigured
## cors
cross-site request config
@ -3239,7 +3268,7 @@ enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
enable [smb](#smb-server) support (**not** recommended): `impacket==0.13.0`
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram
`pyvips` adds support for some additional image formats, but can use extreme amounts of RAM
* to install `pyvips` on Linux: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
* to install `pyvips` on windows: `pip install --user -U "pyvips[binary]"`

View file

@ -626,7 +626,7 @@ class Gateway(object):
"st_atime": ts,
"st_mtime": ts,
"st_ctime": ts,
"st_blocks": int((sz + 511) / 512),
"st_blocks": -int(-sz // 512),
}
def stat_file(self, ts, sz):
@ -638,7 +638,7 @@ class Gateway(object):
"st_atime": ts,
"st_mtime": ts,
"st_ctime": ts,
"st_blocks": int((sz + 511) / 512),
"st_blocks": -int(-sz // 512),
}

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
S_VERSION = "2.22"
S_BUILD_DT = "2026-06-13"
@ -398,7 +398,7 @@ class MTHash(object):
self.csz = chunksz
chunks = {}
nchunks = int(math.ceil(fsz / chunksz))
nchunks = -int(-fsz // chunksz)
for nch in range(nchunks):
self.work_q.put(nch)
@ -725,7 +725,7 @@ def up2k_chunksize(filesize):
stepsize = 512 * 1024
while True:
for mul in [1, 2]:
nchunks = math.ceil(filesize * 1.0 / chunksize)
nchunks = -int(-filesize // chunksize)
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
return chunksize

View file

@ -17,8 +17,12 @@ defaults
listen foo1
bind *:8081
http-request add-header X-Forwarded-Proto https if { ssl_fc }
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
server srv1 127.0.0.1:3923 maxconn 512
listen foo2
bind *:8082
http-request add-header X-Forwarded-Proto https if { ssl_fc }
http-request add-header X-Forwarded-Proto http unless { ssl_fc }
server srv1 /dev/shm/party.sock maxconn 512

View file

@ -285,6 +285,7 @@ in
environment = {
PYTHONUNBUFFERED = "true";
NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
XDG_CONFIG_HOME = externalStateDir;
};

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.
pkgname=copyparty
pkgver="1.20.17"
pkgver="1.20.20"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@ -16,7 +16,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
"python-mutagen: music tags (alternative)"
"python-paramiko: sftp server",
"python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
"python-pyvips: thumbnails for images (eats ram, prefer pillow)"
"libkeyfinder: detection of musical keys"
"python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks"
@ -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")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("949553aeffea942a0ddabd3e12d28a643fea862d78930bb36e0accc66c4c9475")
sha256sums=("a31f9e4a43e56608765e108353140784d50181c37e705e9ce8ed9f3899f57dfa")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.17
pkgver=1.20.20
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("/etc/${pkgname}.d/init" )
sha256sums=("949553aeffea942a0ddabd3e12d28a643fea862d78930bb36e0accc66c4c9475")
sha256sums=("a31f9e4a43e56608765e108353140784d50181c37e705e9ce8ed9f3899f57dfa")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"

View file

@ -30,7 +30,7 @@
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withThumbnails ? true,
# create thumbnails with PyVIPS; even faster, uses more memory
# create thumbnails with PyVIPS; uses more memory, pillow is preferred
# -- can be combined with Pillow to support more filetypes
withFastThumbnails ? false,

View file

@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.20.17/copyparty-1.20.17.tar.gz",
"version": "1.20.17",
"hash": "sha256-lJVTrv/qlCoN2r0+EtKKZD/qhi14kwuzbgrMxmxMlHU="
"url": "https://github.com/9001/copyparty/releases/download/v1.20.20/copyparty-1.20.20.tar.gz",
"version": "1.20.20",
"hash": "sha256-ox+eSkPlZgh2XhCDUxQHhNUBgcN+cF6c6O2fOJn1ffo="
}

View file

@ -16,7 +16,7 @@ let
withMagic = true;
};
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-headless; } // attrs);
in
{
copyparty = call { stable = true; };

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import platform
@ -135,6 +135,7 @@ web/tl/vie.js
web/ui.css
web/up2k.js
web/util.js
web/wopi.html
"""
RES = set(zs.strip().split("\n"))
RESM = {

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
"""copyparty: http file sharing hub (py2/py3)"""
__author__ = "ed <copyparty@ocv.me>"
@ -1042,7 +1042,12 @@ def get_sects():
.1 = when necessary, append a dot followed by a single digit
.1! = counter is always added, even when not necessary
-3 = a hyphen followed by three-digit counter
no = disable counter; overwrite existing logfile
no = disable counter; append to existing file
del = delete existing logfile and create new
over = overwrite existing logfile
append is not possible for .xz-compressed logfiles;
if logfile is *.xz and rlo is no then rlo will be .1
"""
),
],
@ -1329,7 +1334,7 @@ def add_upload(ap):
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=1, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
@ -1427,7 +1432,7 @@ def add_auth(ap):
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=24, help="session key length; default is 144 bits ((24//4)*4*6)")
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
@ -1577,6 +1582,18 @@ def add_opds(ap):
ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")
def add_wopi(ap):
ap2 = ap.add_argument_group("WOPI options")
ap2.add_argument("--wopi", action="store_true", help="enable integration with office suites using WOPI")
ap2.add_argument("--wopi-api", metavar="URL", type=u, default="", help="URL that the WOPI-client should use to communicate with copyparty; default is same as user's webbrowser. Example: [\033[32mhttps://party.example.com/\033[0m]")
ap2.add_argument("--wopi-url", metavar="URL", type=u, default="", help="URL to your WOPI client; the host of e.g. Collabora Online. Example: [\033[32mhttps://code.example.com/\033[0m]")
ap2.add_argument("--wopi-urls", metavar="HOST=URL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m maps http \033[33mHOST\033[0m copyparty being accessed by to specific WOPI client instance \033[33mURL\033[0m; falls back to \033[33m--wopi-url\033[0m; examples: [\033[32mparty.public.com=https://office.public.com/\033[0m], [\033[32mparty.internal.net:8443=https://office.internal.net:8443/\033[0m]")
ap2.add_argument("--wopi-crt", metavar="TXT", type=u, default="", help="if \033[33m--wopi-url\033[0m is selfsigned: path to ca.pem or cert.pem to expect/verify (can be [\033[32mno\033[0m] for full-yolo)")
ap2.add_argument("--wopi-crt-icn", action="store_true", help="if \033[33m--wopi-url\033[0m is selfsigned: ignore the CN (server ip/name) in cert")
ap2.add_argument("--wopi-ttl", metavar="SEC", type=int, default=1800, help="session lifetime; allow editing for this many seconds (default is 30 min)")
ap2.add_argument("--wopi-wdel", action="store_true", help="require permissions read+write+delete for writing to file; default is just read+write")
def add_handlers(ap):
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file")
@ -1711,6 +1728,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("--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)")
if FULL_HELP or ANYWIN:
ap2.add_argument("--unsafe-tools", action="store_true", help="windows-only: allow running programs (ffmpeg, dcraw_emu, ...) when their location is inside a folder that can be uploaded to (dangerous due to DLL-hijacking)")
if FULL_HELP or (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)")
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help="a/n/f; [\033[32ma\033[0m]=auto (yes if the program 'bwrap' exists (assumes it works)), [\033[32mn\033[0m]=no (assumes bwrap is broken), [\033[32mf\033[0m]=force (disables FFmpeg if bwrap unavailable)")
@ -1744,7 +1763,7 @@ def add_logging(ap):
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
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("--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="no", 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("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
@ -1795,7 +1814,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="webp/jpg thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)")
ap2.add_argument("--th-qvx", metavar="N", type=int, default=64, help="jxl thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qvx)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,raw,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-dec", metavar="LIBS", default="raw,pil,ff,vips", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output")
@ -1807,21 +1826,21 @@ def add_thumbnail(ap):
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
ap2.add_argument("--th-pregen", metavar="F,F", type=u, default="", help="pregenerate thumbnails on startup; \033[33mF,F\033[0m is comma-separated list of formats; example: [\033[32mj,jf,w,w3,wf,wf3,x,xf\033[0m] NOTE: remember to set \033[33m--th-maxage 123456789\033[0m (volflag=th_pregen)")
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 (volflag=th_covers)")
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://github.com/libvips/libvips
# 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:'
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,kra,ora,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,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,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,kra,ora,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-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("--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, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
def add_transcoding(ap):
@ -1882,7 +1901,8 @@ def add_db_general(ap, hcores):
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 3x), and NOTE: only applies to filenames/paths, not tags")
ap2.add_argument("--srch-nfkc", action="store_true", help="case-insensitive and unicode-normalization-insensitive search (NFC/NFD) for filenames/paths; slightly slower than \033[33m--srch-icase\033[0m (about 15%% slower / 87%% as fast)")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
@ -2092,6 +2112,7 @@ def run_argparse(
add_tftp(ap)
add_smb(ap)
add_opds(ap)
add_wopi(ap)
add_safety(ap)
add_salt(ap, fk_salt, dk_salt, ah_salt)
add_optouts(ap)

View file

@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 18)
VERSION = (1, 20, 20)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 7, 9)
BUILD_DT = (2026, 8, 4)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import base64
@ -2612,6 +2612,13 @@ class AuthSrv(object):
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
self.log(t % (vol.vpath, etv), 3)
zsl = [x.strip() for x in vol.flags["th_covers"].split(",")]
zsl = [x for x in zsl if x not in ("", "no")]
vol.flags["th_coversl"] = zsl
vol.flags["th_coversd"] = zsl + ["." + x for x in zsl]
vol.flags["th_covers_set"] = set(vol.flags["th_coversl"])
vol.flags["th_coversd_set"] = set(vol.flags["th_coversd"])
zsl = [x.strip() for x in vol.flags["rw_edit"].split(",")]
zsl = [x for x in zsl if x]
vol.flags["rw_edit"] = ",".join(zsl)
@ -2653,10 +2660,11 @@ class AuthSrv(object):
if head_s and not head_s.endswith("\n"):
head_s += "\n"
zs2 = "Content-Security-Policy: %s\r\n"
zs = vol.flags.get("csp_ui", "")
csp_ui = "Content-Security-Policy: %s\r\n" % (zs,) if zs else ""
csp_ui = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
zs = vol.flags.get("csp_dl", "")
csp_dl = "Content-Security-Policy: %s\r\n" % (zs,) if zs else ""
csp_dl = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
zs = "X-Content-Type-Options: nosniff\r\n"
if "norobots" in vol.flags:
@ -2754,7 +2762,8 @@ class AuthSrv(object):
for k in drop:
t = 'cannot enable [%s] for volume "/%s" because this requires one of the following: e2d / e2ds / e2dsa (either as volflag or global-option)'
self.log(t % (k, vol.vpath), 1)
if not (enshare and vp.startswith(shrs)):
self.log(t % (k, vol.vpath), 1)
vol.flags.pop(k)
zi = vol.flags.get("lifetime") or 0
@ -2863,12 +2872,13 @@ class AuthSrv(object):
continue
try:
bos.makedirs(vol.realpath, vf=vol.flags)
files = os.listdir(vol.realpath)
for fn in files:
for fn, _ in statdir(
self.log_func, not self.args.no_scandir, False, vol.realpath, True
):
fn2 = fn.lower()
if fn == fn2:
fn2 = fn.upper()
if fn == fn2 or fn2 in files:
if fn == fn2:
continue
is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
ccs = "y" if is_ci else "n"
@ -3037,6 +3047,10 @@ class AuthSrv(object):
t = "hint: enable upload deduplication with --dedup (but see readme for consequences)"
self.log(t, 6)
if MACOS and not self.args.srch_nfkc:
t = "hint: enable --srch-nfkc to improve unicode filename/path search on MacOS"
self.log(t, 6)
zv, _ = vfs.get("/", "*", False, False)
zs = zv.realpath.lower()
if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"):
@ -3047,6 +3061,8 @@ class AuthSrv(object):
zv, _ = vfs.get("", "*", False, True, err=999)
if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath:
t = "anyone can write to the current directory: {}\n"
if ANYWIN:
t += "/!\\ NOTE: because you are using Windows, this is kinda dangerous (DLL-hijacking); you should configure accounts and volumes if this is accessible from an untrusted network\n"
self.log(t.format(zv.realpath), c=1)
self.warn_anonwrite = False
@ -3240,6 +3256,7 @@ class AuthSrv(object):
self.js_ls = {}
self.js_htm = ""
self.md_htm = ""
for vp, vn in self.vfs.all_nodes.items():
if enshare and vp.startswith(shrs):
continue # propagates later in this func
@ -3331,6 +3348,8 @@ class AuthSrv(object):
zs2 = getattr(self.args, zs, "")
if zs2:
js_htm[zs] = zs2
if self.args.wopi:
js_htm["have_wopi"] = True
zs = "have_emp md_no_br"
md_htm = {x: js_htm[x] for x in zs.split(" ")}
@ -3356,6 +3375,7 @@ class AuthSrv(object):
dbv = vol.get_dbv("")[0]
vol.js_ls = vol.js_ls or dbv.js_ls or {}
vol.js_htm = vol.js_htm or dbv.js_htm or "{}"
vol.md_htm = vol.md_htm or dbv.md_htm or "{}"
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
vol.flags["tcolor"] = zs.lstrip("#")
@ -3783,12 +3803,12 @@ class AuthSrv(object):
"",
]
csv = set("i p th_covers zm_on zm_off zs_on zs_off".split())
csv = set("i p zm_on zm_off zs_on zs_off".split())
zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm"
lst = set(zs.split())
askip = set("a v c vc cgen exp_lg exp_md theme".split())
t = "exp_lg exp_md ext_th_d mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
t = "emb_all emb_lgs emb_mds exp_lg exp_md ext_th_d ls_q_m mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set"
fskip = set(t.split())
# keymap from argv to vflag
@ -3915,6 +3935,9 @@ class AuthSrv(object):
except:
pass
if k in ("chmod_d", "chmod_f"):
v = "%o" % (v,)
try:
ak = vmap[k]
v2 = getattr(self.args, ak)
@ -3924,7 +3947,7 @@ class AuthSrv(object):
except:
pass
if v2 is v:
if v2 == v:
continue
except:
pass
@ -3968,7 +3991,7 @@ class AuthSrv(object):
def derive_args(args: argparse.Namespace) -> None:
args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)
args.have_ipu_or_ipr = bool(args.ipu or args.ipr)
args.have_ipu_or_ipr = bool(args.ipu or args.ipr or args.wopi)
def n_du_who(s: str) -> int:

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import time

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import threading
import time

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import os

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import threading

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
# awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' '
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
@ -157,6 +157,7 @@ def vf_vmap() -> dict[str, str]:
"tail_tmax",
"tail_who",
"tcolor",
"th_covers",
"th_pregen",
"th_qv",
"th_qvx",
@ -321,6 +322,7 @@ flagcats = {
"th3x": "3x resolution (y/n/fy/fn)",
"th_qv=40": "webp/jpg thumbnail quality (10~90)",
"th_qvx=40": "jxl thumbnail quality (10~90)",
"th_covers=a.jpg,a.png": "filenames to prefer as folder-thumbs",
"convt": "convert-to-image timeout in seconds",
"aconvt": "convert-to-audio timeout in seconds",
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import importlib
import sys

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import json
@ -248,7 +248,7 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
log("vfs", t + "".join(t2) + "\n", 1)
assume = "mac" if MACOS else "lin"
assume = "mac" if MACOS else "win" if ANYWIN else "lin"
for vol in asrv.vfs.all_nodes.values():
if not vol.realpath or vol.flags.get("is_file"):
continue
@ -263,3 +263,4 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
htm = json.loads(vol.js_htm)
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
vol.js_htm = json_hesc(json.dumps(htm))
# md_htm is dontcare; only relevant for up2k request body

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import errno
@ -149,6 +149,7 @@ class FtpFs(AbstractedFS):
self.hub: "SvcHub" = cmd_channel.hub
self.args = cmd_channel.args
self.uname = cmd_channel.uname
self.log_src = "%s:FTP" % (cmd_channel.cli_ip,)
self.cwd = "/" # pyftpdlib convention of leading slash
self.root = "/var/lib/empty"
@ -156,8 +157,8 @@ class FtpFs(AbstractedFS):
self.listdirinfo = self.listdir
self.chdir(".")
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log("ftpd", msg, c)
def clog(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log(self.log_src, msg, c)
def v2a(
self,
@ -238,6 +239,8 @@ class FtpFs(AbstractedFS):
return True
def open(self, filename: str, mode: str) -> typing.IO[Any]:
self.clog("open(%r,%r) @%s" % (filename, mode, self.uname))
r = "r" in mode
w = "w" in mode or "a" in mode or "+" in mode
@ -255,7 +258,7 @@ class FtpFs(AbstractedFS):
xbu = vfs.flags.get("xbu")
if xbu:
hr = runhook(
self.log,
self.clog,
None,
self.hub.up2k,
"xbu.ftp",
@ -275,7 +278,7 @@ class FtpFs(AbstractedFS):
if t or hr.get("rc") != 0:
if not t:
t = "upload blocked by xbu server config: %r" % (filename,)
self.log(t, 3)
self.clog(t, 3)
raise FSE(t)
if w and need_unlink: # type: ignore # !rm
@ -300,12 +303,13 @@ class FtpFs(AbstractedFS):
# Don't unlink file for append mode
elif "a" not in mode:
wunlink(self.log, ap, VF_CAREFUL)
wunlink(self.clog, ap, VF_CAREFUL)
ret = open(fsenc(ap), mode, self.args.iobuf)
if w and "fperms" in vfs.flags:
set_fperms(ret, vfs.flags)
self.clog(" `-- %r" % (ap,))
return ret
def chdir(self, path: str) -> None:
@ -331,10 +335,12 @@ class FtpFs(AbstractedFS):
self.cwd = nwd
def mkdir(self, path: str) -> None:
self.clog("mkdir(%r) @%s" % (path, self.uname))
ap, vfs, _ = self.rv2a(path, w=True)
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
def listdir(self, path: str) -> list[str]:
self.clog("ls(%r) @%s" % (path, self.uname))
vpath = join(self.cwd, path)
try:
ap, vfs, rem = self.v2a(vpath, True, False)
@ -384,6 +390,7 @@ class FtpFs(AbstractedFS):
return ret
def rmdir(self, path: str) -> None:
self.clog("rmdir(%r) @%s" % (path, self.uname))
ap = self.rv2a(path, d=True)[0]
try:
bos.rmdir(ap)
@ -392,6 +399,7 @@ class FtpFs(AbstractedFS):
raise
def remove(self, path: str) -> None:
self.clog("rm(%r) @%s" % (path, self.uname))
if self.args.no_del:
raise FSE("The delete feature is disabled in server config")
@ -402,6 +410,7 @@ class FtpFs(AbstractedFS):
raise FSE(str(ex))
def rename(self, src: str, dst: str) -> None:
self.clog("mv(%r,%r) @%s" % (src, dst, self.uname))
if self.args.no_mv:
raise FSE("The rename/move feature is disabled in server config")
@ -431,6 +440,7 @@ class FtpFs(AbstractedFS):
return st
def utime(self, path: str, timeval: float) -> None:
self.clog("utime(%r,%r) @%s" % (path, timeval, self.uname))
ap = self.rv2a(path, w=True)[0]
bos.utime_c(logging.warning, ap, int(timeval), False)
@ -438,6 +448,9 @@ class FtpFs(AbstractedFS):
ap = self.rv2a(path)[0]
return bos.stat(ap)
def readlink(self, *a) -> None:
raise OSError(errno.EINVAL, "nope")
def isfile(self, path: str) -> bool:
try:
st = self.stat(path)
@ -480,9 +493,13 @@ class FtpFs(AbstractedFS):
def get_user_by_uid(self, uid: int) -> str:
return "root"
def get_group_by_uid(self, gid: int) -> str:
def get_group_by_gid(self, gid: int) -> str:
return "root"
def mkstemp(self, *a, **ka):
self.clog("mkstemp%r @%s" % (a, self.uname))
raise FSE("nope")
class FtpHandler(FTPHandler):
abstracted_fs = FtpFs
@ -511,6 +528,7 @@ class FtpHandler(FTPHandler):
return
self.cli_ip = cip
self.log_src = "%s:FTP" % (cip,)
# abspath->vpath mapping to resolve log_transfer paths
self.vfs_map: dict[str, str] = {}
@ -518,6 +536,15 @@ class FtpHandler(FTPHandler):
# reduce non-debug logging
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
def clog(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log(self.log_src, msg, c)
def ftp_NOOP(self, *a) -> None:
self.respond("200 pong", logging.debug)
def ftp_STOU(self, *a) -> None:
self.respond("550 nope", logging.warning)
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
@ -530,7 +557,7 @@ class FtpHandler(FTPHandler):
xbu = vfs.flags.get("xbu")
if xbu:
hr = runhook(
None,
self.clog,
None,
self.hub.up2k,
"xbu.ftpd",

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse # typechk
import copy
@ -30,7 +30,7 @@ try:
except:
pass
from .__init__ import ANYWIN, RES, RESM, TYPE_CHECKING, EnvParams, unicode
from .__init__ import ANYWIN, PY2, RES, RESM, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION
from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos
@ -149,6 +149,11 @@ if True: # pylint: disable=using-constant-test
if TYPE_CHECKING:
from .httpconn import HttpConn
if PY2:
from urllib2 import urlopen
else:
from urllib.request import urlopen
if not hasattr(socket, "AF_UNIX"):
setattr(socket, "AF_UNIX", -9001)
@ -171,6 +176,27 @@ RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"}
ACODE2_FMT = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
IDX_HTML = set(["index.htm", "index.html"])
# coolwsd-26.04.2.3 discovery editnew; browser.js wopi_set is superset of this
WOPI_NEW = (
".odt",
".fodt",
".ott",
".docx",
".dotx",
".rtf",
".odm",
".ods",
".fods",
".ots",
".xlsx",
".odp",
".fodp",
".otp",
".pptx",
".fodg",
".otg",
)
A_FILE = os.stat_result(
(0o644, -1, -1, 1, 1000, 1000, 8, 0x39230101, 0x39230101, 0x39230101)
)
@ -813,6 +839,22 @@ class HttpCli(object):
if not ipr[self.uname].map(self.ip):
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
self.uname = "*"
if self.args.wopi and "access_token" in self.uparam:
wopi_a = self.uparam["access_token"]
try:
wopi_f = self.conn.hsrv.wopi_files[wopi_a]
if wopi_f["expires"] < time.time():
raise Exception("expired")
uname = wopi_f["uname"]
self.asrv.vfs.get(
wopi_f["vp"], uname, True, True, False, self.args.wopi_wdel
)
self.uname = uname
except Exception as ex:
self.conn.hsrv.wopi_files.pop(wopi_a, None)
self.cbonk(self.conn.hsrv.gpwd, wopi_a, "wopi", "bad wopi tokens")
self.loud_reply("bad wopi token %s (%s)" % (wopi_a, ex), status=400)
return False
self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname]
@ -1532,8 +1574,141 @@ class HttpCli(object):
if "rss" in self.uparam:
return self.tx_rss()
if self.args.wopi:
if "wopi" in self.uparam:
return self.tx_wopi()
if self.vpath.startswith("wopi"):
return self.tx_wopi_api()
return self.tx_browser()
def tx_wopi_api(self) -> bool:
atoken = self.uparam["access_token"]
session = self.conn.hsrv.wopi_files[atoken]
if self.do_log:
self.log(" `-- wopi: %r" % (session["vp"],))
zs = "wopi/files/%s" % (session["file_id"],)
if not self.vpath.startswith(zs):
return self.tx_404()
query = self.vpath[len(zs) :]
vfs, rem = self.asrv.vfs.get(session["vp"], self.uname, True, True)
vpath = vjoin(vfs.vpath, rem)
ap = vfs.canonical(rem)
if query.startswith("/contents"):
return self.tx_file("oh_f", ap)
else:
st = bos.stat(ap)
file_info = {
"BaseFileName": vpath.split("/")[-1],
"Size": st.st_size,
"OwnerId": self.uname,
"UserId": self.uname,
"UserFriendlyName": self.uname,
"UserCanWrite": True,
"UserCanNotWriteRelative": True,
"LastModifiedTime": time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(st.st_mtime)
),
}
ret = json.dumps(file_info).encode("utf-8", "replace")
self.reply(ret, 200, "application/json; charset=utf-8")
return True
return self.tx_404()
def tx_wopi(self) -> bool:
vpath = vjoin(self.vpath, self.uparam["wopi"])
vfs, rem = self.asrv.vfs.get(
vpath, self.uname, True, True, False, self.args.wopi_wdel
)
if not bos.path.isfile(vfs.canonical(rem)):
return self.tx_404()
wopi_files = self.conn.hsrv.wopi_files
found = None
rm = []
with self.conn.hsrv.mutex:
now = time.time()
for atoken, session in wopi_files.items():
if session["expires"] < now:
rm.append(atoken)
continue
if session["vp"] != vpath or session["uname"] != self.uname:
continue
if session["expires"] - now < self.args.wopi_ttl * 0.9:
rm.append(atoken)
continue
found = session
break
for zs in rm:
del wopi_files[zs]
if len(wopi_files) > 9000: # about 6 MiB
raise Pebkac(500, "too many wopi sessions")
if not found:
atoken = ub64enc(os.urandom(18)).decode("ascii") # 18 = 144b = 24c
file_id = ub64enc(os.urandom(15)).decode("ascii") # 15 = 120b = 20c
wopi_files[atoken] = session = {
"vp": vpath,
"uname": self.uname,
"file_id": file_id,
"expires": time.time() + self.args.wopi_ttl,
}
xml = "?"
try:
from .dxml import parse_xml
uo_kw = {}
if self.args.wopi_crt:
import ssl
if self.args.wopi_crt == "no":
ctx = ssl._create_unverified_context()
else:
ctx = ssl.create_default_context(cafile=self.args.wopi_crt)
ctx.check_hostname = not self.args.wopi_crt_icn
uo_kw["context"] = ctx
wopi_urls = dict(x.lower().split("=", 1) for x in self.args.wopi_urls or [])
url = wopi_urls.get(self.host.lower(), self.args.wopi_url).rstrip("/")
url += "/hosting/discovery"
buf = urlopen(url, **uo_kw).read()
xml = buf.decode("ascii", "replace").lower()
enc = self.get_xml_enc(xml)
xml = buf.decode(enc, "replace")
xroot = parse_xml(xml)
ext = vpath.split(".")[-1]
url = xroot.find(
".//action[@ext='%s'][@name='edit'][@urlsrc]" % (ext,)
).get("urlsrc")
if not url.endswith(("?", "&")):
url += "&" if "?" in url else "?"
url += "WOPISrc="
if self.args.wopi_api:
zs = self.args.wopi_api.rstrip("/")
else:
zs = ("https://" if self.is_https else "http://") + self.host
url += quotep(zs + "/wopi/files/" + session["file_id"])
except:
del wopi_files[atoken] # dont reuse an atoken wopi-client doesnt like
self.log("reading WOPI-client response failed; %s\n%s" % (min_ex(), xml), 3)
raise Pebkac(500, "wopi error (see fileserver log)")
html = self.j2s(
"wopi",
title=self.uparam["wopi"],
url=url,
atoken=atoken,
ttl=session["expires"],
).encode("utf-8", "replace")
self.reply(html, 200, "text/html; charset=utf-8")
return True
def tx_rss(self) -> bool:
if self.do_log:
self.log("RSS %s @%s" % (self.req, self.uname))
@ -2387,6 +2562,9 @@ class HttpCli(object):
raise Pebkac(405, "POST(%r) is disabled in server config" % (ctype,))
if self.args.wopi and self.vpath.startswith("wopi"):
return self.handle_post_binary()
raise Pebkac(405, "don't know how to handle POST(%r)" % (ctype,))
def handle_smsg(self) -> bool:
@ -3156,6 +3334,9 @@ class HttpCli(object):
except:
raise Pebkac(400, "you must supply a content-length for binary POST")
if self.args.wopi and self.vpath.startswith("wopi"):
return self.rx_wopi(postsize)
try:
chashes = self.headers["x-up2k-hash"].split(",")
wark = self.headers["x-up2k-wark"]
@ -3367,6 +3548,47 @@ class HttpCli(object):
self.reply(b"thank")
return True
def rx_wopi(self, postsize: int) -> bool:
atoken = self.uparam["access_token"]
session = self.conn.hsrv.wopi_files[atoken]
if self.do_log:
self.log(" `-- wopi: %r" % (session["vp"],))
zs = "wopi/files/%s/contents" % (session["file_id"],)
if not self.vpath.startswith(zs):
return self.tx_404()
vpath = self.conn.hsrv.wopi_files[self.uparam["access_token"]]["vp"]
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
vpath = vjoin(vfs.vpath, rem)
ap = vfs.canonical(rem)
st = bos.stat(ap)
if "x-cool-wopi-timestamp" in self.headers:
zs = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(st.st_mtime))
if self.headers["x-cool-wopi-timestamp"] != zs:
self.reply(json.dumps({"COOLStatusCode": 1010}).encode("utf-8"), 409)
return True
buf = b""
for rbuf in self.get_body_reader()[0]:
buf += rbuf
if not rbuf:
break
if len(buf) != postsize:
t = "wopi post with incorrect length; expected %d, got %d"
raise Pebkac(400, t % (postsize, len(buf)))
with open(ap, "wb") as file:
file.write(buf)
st = bos.stat(ap)
zs = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(st.st_mtime))
ret = json.dumps({"LastModifiedTime": zs}).encode("utf-8", "replace")
self.reply(ret, 200, "application/json; charset=utf-8")
return True
def handle_chpw(self) -> bool:
assert self.parser # !rm
if self.args.usernames:
@ -3521,7 +3743,7 @@ class HttpCli(object):
def _mkdir(self, vpath: str, dav: bool = False) -> bool:
nullwrite = self.args.nw
self.gctx = vpath
vpath = undot(vpath)
vpath = sanitize_vpath(undot(vpath))
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
if "nosub" in vfs.flags:
raise Pebkac(403, "mkdir is forbidden below this folder")
@ -3627,6 +3849,9 @@ class HttpCli(object):
True,
)
if self.args.wopi and sanitized.endswith(WOPI_NEW):
return self.redirect(vjoin(vfs.vpath, rem), "?wopi=" + quotep(sanitized))
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.redirect(vpath, "?edit")
return True
@ -6856,7 +7081,8 @@ class HttpCli(object):
nothumb = "dthumb" in dbv.flags
if is_dir:
vrem = vrem.rstrip("/")
if nothumb:
cvs = dbv.flags["th_coversl"]
if nothumb or not cvs:
pass
elif icur and vrem:
q = "select fn from cv where rd=? and dn=?"
@ -6873,7 +7099,7 @@ class HttpCli(object):
except:
pass
else:
for fn in self.args.th_covers:
for fn in cvs:
fp = os.path.join(abspath, fn)
try:
st = bos.stat(fp)
@ -7219,20 +7445,18 @@ class HttpCli(object):
dirs = []
files = []
ptn_hr = RE_HR
use_abs_url = is_opds or (
vpath and not is_ls and not is_js and not self.trailing_slash
)
base = ""
if is_opds or (vpath and not (is_ls or is_js or self.trailing_slash)):
if is_opds:
base = self.args.SRS
if vpath:
base += vpath + "/"
else:
base = "/" + vpath + "/"
for fn in ls_names:
base = ""
href = fn
if use_abs_url:
if is_opds:
base = self.args.SRS
if vpath:
base += vpath + "/"
else:
base = "/" + vpath + "/"
href = base + fn
href = base + fn
if fn in vfs_virt:
fspath = vfs_virt[fn].realpath
@ -7402,6 +7626,26 @@ class HttpCli(object):
(fe["sz"], fe["tags"][".files"]) = hit
except:
pass # 404 or mojibake
if vfs_virt:
q = "select sz, nf from ds where rd='' limit 1"
try:
for fe in [x for x in dirs if x["name"] in vfs_virt]:
if ".files" not in fe["tags"]:
fe["tags"][".files"] = 0
vols = [vn.nodes[fe["name"]]]
while vols:
vn2 = vols.pop()
if self.uname not in vn2.axs.uread:
continue
vols += list(vn2.nodes.values())
if vn2.dbv not in (vn2, None):
continue
hit = idx.get_cur(vn2).execute(q).fetchone()
if hit:
fe["sz"] += hit[0]
fe["tags"][".files"] += hit[1]
except:
pass
taglist = [k for k in lmte if k in tagset]
else:
@ -7636,7 +7880,7 @@ class HttpCli(object):
self.conn.hsrv.j2[tpl] = j2env.get_template(tname)
thumb = ""
is_pic = is_vid = is_au = False
for fn in self.args.th_coversd:
for fn in vn.flags["th_coversd"]:
if fn in lnames:
thumb = lnames[fn]
break

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse # typechk
import os

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import hashlib
import math
@ -180,6 +180,8 @@ class HttpSrv(object):
self.u2idx_free: dict[str, U2idx] = {}
self.u2idx_n = 0
self.wopi_files: dict[str, dict[str, str]] = {}
assert jinja2 # type: ignore # !rm
env = jinja2.Environment()
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
@ -195,6 +197,7 @@ class HttpSrv(object):
"shares",
"splash",
"svcs",
"wopi",
]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.j2["opds"] = env.get_template("opds.xml")
@ -315,7 +318,7 @@ class HttpSrv(object):
self.srvs.append(sck)
self.bound.add((ip, port))
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
self.nclimax = -int(-self.args.nc // nlisteners)
Daemon(
self.thr_listen,
"httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port),

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse # typechk
import colorsys

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import os

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import json
import time

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import json
@ -17,7 +17,9 @@ from .util import (
FFMPEG_URL,
LOG,
REKOBO_LKEY,
SCWD,
VF_CAREFUL,
absreal,
expand_osenv_c,
fsenc,
gzip,
@ -47,10 +49,10 @@ except:
HAVE_MUTAGEN = False
def have_ff(name: str) -> bytes:
def have_ff(name: str) -> list[bytes]:
uname = name.upper()
if os.environ.get("PRTY_NO_" + uname):
return b""
return []
ebin = os.environ.get("PRTY_%s_BIN" % (uname,))
try:
@ -66,11 +68,12 @@ def have_ff(name: str) -> bytes:
bcmd = scmd.encode("utf-8")
try:
sp.Popen([bcmd, b"-version"], stdout=sp.PIPE, stderr=sp.PIPE).communicate()
return bcmd
return [bcmd]
except:
return b""
return []
else:
return (shutil.which(scmd) or "").encode("utf-8")
ret = shutil.which(scmd)
return [absreal(ret).encode("utf-8")] if ret else []
HAVE_FFMPEG = have_ff("ffmpeg")
@ -191,6 +194,12 @@ def au_unpk(
log(t)
fi = zf.open(using)
elif pk == "kra" or pk == "ora":
import zipfile
zf = zipfile.ZipFile(abspath, "r")
fi = zf.open("mergedimage.png")
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
assert fi # !rm
@ -247,14 +256,14 @@ def ffprobe(
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
# ffprobe -hide_banner -show_streams -show_format --
bap = fsenc(abspath)
cmd = bwrap(HAVE_FFPROBE, bap, b"") + [
cmd = bwrap(HAVE_FFPROBE[0], bap, b"") + [
b"-hide_banner",
b"-show_streams",
b"-show_format",
b"--",
bap,
]
rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200)
rc, so, se = runcmd(cmd, cwd=SCWD, timeout=timeout, nice=True, oom=200)
if rc and TH_BWRAP:
bwrap_fail(se)
retchk(rc, cmd, se)

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import socket
import time

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import base64

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import hashlib

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import re

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import re
import stat

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import tempfile

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import atexit
@ -81,6 +81,7 @@ from .util import (
alltrace,
build_netmap,
expat_ver,
fsenc,
gzip,
html_escape,
load_ipr,
@ -233,10 +234,14 @@ class SvcHub(object):
self.lo1 = self.lo2 = ""
if args.lo:
do_xz = args.lo.replace("%R", "").lower().endswith(".xz")
if "%R" not in args.lo:
args.lo += "%R"
if args.rlo in ("", "no"):
args.rlo = ""
if do_xz and not args.rlo:
args.rlo = ".1"
if args.rlo in ("", "del", "over"):
args.lo = args.lo.replace("%R", "")
try:
self.lo1, self.lo2 = args.lo.split("%R")
@ -396,6 +401,9 @@ class SvcHub(object):
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
ramdisk_chk(self.asrv)
if ANYWIN and not self.args.unsafe_tools:
self._check_toolpaths()
if args.cgen:
self.asrv.cgen()
@ -1021,6 +1029,18 @@ class SvcHub(object):
if zb:
Daemon(self.s6_notify, "s6-notify", (zb,))
def _check_toolpaths(self) -> None:
for vol in self.asrv.vfs.all_vols.values():
if not vol.realpath or not vol.axs.uwrite:
continue
ap_vol = (vol.realpath + os.sep).encode("utf-8")
for zsl in (HAVE_FFMPEG, HAVE_FFPROBE, HAVE_DCRAW):
if zsl and zsl[0].startswith(ap_vol):
zs = zsl[0].decode("utf-8", "replace")
t = "will not use [%s] because it is inside a writable volume [/%s] => [%s] and --unsafe-tools is not enabled"
self.log("root", t % (zs, vol.vpath, vol.realpath), 3)
del zsl[0]
def _feature_test(self) -> None:
fok = []
fng = []
@ -1029,8 +1049,8 @@ class SvcHub(object):
# fmt: off
to_check = [
(HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"),
(HAVE_PIL, "pillow", "image thumbnails (plenty fast)"),
(HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"),
(HAVE_PIL, "pillow", "image thumbnails (good and fast)"),
(HAVE_VIPS, "vips", "image thumbnails (eats ram; will prefer pillow)"),
(H_PIL_WEBP, "pillow-webp", "create thumbnails as webp files"),
(HAVE_FFMPEG, "ffmpeg", t_ff + ", good-but-slow image thumbnails"),
(HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"),
@ -1107,14 +1127,19 @@ class SvcHub(object):
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
self.log("root", t, 3)
if not self.args.no_dav:
if self.args.wopi or not self.args.no_dav:
from .dxml import DXML_OK
if not DXML_OK:
if not self.args.no_dav:
self.args.no_dav = True
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
self.args.wopi = False
self.args.no_dav = True
t = "WARNING:\nDisabling WebDAV and WOPI support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
if self.args.wopi and self.args.j != 1:
self.args.wopi = False
t = "WARNING: Disabling --wopi because -j is not 1 (the default and recommended -j value)"
self.log("root", t, 1)
if (
not E.scfg
@ -1183,14 +1208,6 @@ class SvcHub(object):
if al.rsp_jtr:
al.rsp_slp = 0.000001
zsl = al.th_covers.split(",")
zsl = [x.strip() for x in zsl]
zsl = [x for x in zsl if x]
al.th_covers = zsl
al.th_coversd = zsl + ["." + x for x in zsl]
al.th_covers_set = set(al.th_covers)
al.th_coversd_set = set(al.th_coversd)
zs = al.use_bwrap.strip().lower()
if zs == "a":
if not HAVE_BWRAP:
@ -1446,7 +1463,7 @@ class SvcHub(object):
def _setup_logfile(self) -> None:
base_fn = fn = self._logname()
sel_fn = fn + self.lo2
do_xz = sel_fn.lower().endswith(".xz")
do_xz = sel_fn.replace("%R", "").lower().endswith(".xz")
if "%R" in self.args.lo:
# yup this is a race; if started sufficiently concurrently, two
# copyparties can grab the same logfile (considered and ignored)
@ -1462,18 +1479,31 @@ class SvcHub(object):
except:
pass
if self.args.rlo in ("del", "over"):
tmode = "wt"
bmode = "w"
else:
tmode = "at"
bmode = "a"
if self.args.rlo == "del":
try:
bos.unlink(fn)
except:
pass
try:
if do_xz:
import lzma
lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0)
lh = lzma.open(fn, tmode, encoding="utf-8", errors="replace", preset=0)
self.args.no_logflush = True
else:
lh = open(fn, "wt", encoding="utf-8", errors="replace")
lh = open(fn, tmode, encoding="utf-8", errors="replace")
except:
import codecs
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
lh = codecs.open(fn, bmode, encoding="utf-8", errors="replace")
if getattr(self.args, "free_umask", False):
os.fchmod(lh.fileno(), 0o644)
@ -1488,6 +1518,8 @@ class SvcHub(object):
printed = "".join(lprinted) + msg
t = "t0: {:.3f}\nargv: {}\n\n{}"
lh.write(t.format(self.E.t0, " ".join(argv), printed))
if not self.args.no_logflush:
lh.flush()
self.logf = lh
self.logf_base_fn = base_fn
print(msg, end="")

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import calendar
import stat

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import re

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
try:
from types import SimpleNamespace

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import os

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import hashlib
import io
@ -31,6 +31,7 @@ from .mtag import (
from .util import BytesIO # type: ignore
from .util import (
FFMPEG_URL,
SCWD,
VF_CAREFUL,
Cooldown,
Daemon,
@ -774,7 +775,7 @@ class ThumbSrv(object):
self.wait4ram(0.6, tpath)
bap = fsenc(abspath)
# fmt: off
cmd = bwrap(HAVE_DCRAW, bap, b"") + [
cmd = bwrap(HAVE_DCRAW[0], bap, b"") + [
b"-h", # halfsize
b"-o", b"1", # srgb
b"-s", b"0", # first frame
@ -782,7 +783,7 @@ class ThumbSrv(object):
bap,
]
# fmt: on
p = sp.Popen(cmd, stdout=sp.PIPE)
p = sp.Popen(cmd, stdout=sp.PIPE, cwd=SCWD)
try:
if HAVE_PIL:
self.conv_image_pil(Image.open(p.stdout), tpath, fmt, vn)
@ -833,7 +834,7 @@ class ThumbSrv(object):
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret:
return
@ -841,6 +842,13 @@ class ThumbSrv(object):
# audio in a video trenchcoat
return self.conv_spec(abspath, tpath, fmt, vn)
for strm in strms:
if (
strm.get("codec_type") == "video"
and strm.get("DISPOSITION:attached_pic") == "1"
):
return self.conv_emb_cv(abspath, tpath, fmt, vn, strm)
ext = abspath.rsplit(".")[-1].lower()
if ext in ["h264", "h265"] or ext in self.fmt_ffi:
seek: list[bytes] = []
@ -870,7 +878,7 @@ class ThumbSrv(object):
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner"
@ -911,7 +919,7 @@ class ThumbSrv(object):
def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
ret, _, serr = runcmd(cmd, cwd=SCWD, timeout=vn.flags[kto], nice=True, oom=oom)
if not ret:
return
@ -1013,7 +1021,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1094,7 +1102,7 @@ class ThumbSrv(object):
bap_out = fsenc(infile)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1128,7 +1136,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1166,7 +1174,7 @@ class ThumbSrv(object):
# to not support opus then it's probably also super picky
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1197,7 +1205,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1234,7 +1242,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1297,7 +1305,7 @@ class ThumbSrv(object):
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1340,7 +1348,7 @@ class ThumbSrv(object):
bap_out = fsenc(tmp_opus)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1367,7 +1375,7 @@ class ThumbSrv(object):
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
@ -1389,7 +1397,7 @@ class ThumbSrv(object):
bap_in = fsenc(tmp_opus)
bap_out = fsenc(tpath)
# fmt: off
cmd = bwrap(HAVE_FFMPEG, bap_in, bap_out) + [
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import calendar
import os
@ -28,6 +28,14 @@ from .util import (
if HAVE_SQLITE3:
import sqlite3
try:
if os.environ.get("PRTY_NO_UNIDATA"):
raise Exception()
import unicodedata
except:
pass
try:
from pathlib import Path
except:
@ -43,6 +51,14 @@ if PY2:
range = xrange # type: ignore
def norm_icase(txt: str) -> str:
return txt.casefold() if txt else txt
def norm_nfkc(txt: str) -> str:
return unicodedata.normalize("NFKC", txt).casefold() if txt else txt
class U2idx(object):
def __init__(self, hsrv: "HttpSrv") -> None:
self.log_func = hsrv.log
@ -55,6 +71,10 @@ class U2idx(object):
return
if self.args.srch_icase:
if self.args.srch_nfkc:
self.normfun = norm_nfkc
else:
self.normfun = norm_icase
self._open_db = self._open_db_icase
else:
self._open_db = self._open_db_std
@ -82,7 +102,7 @@ class U2idx(object):
def _open_db_icase(self, *args, **kwargs):
db = self._open_db_std(*args, **kwargs)
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
db.create_function("casefold", 1, self.normfun)
return db
def shutdown(self) -> None:
@ -323,7 +343,7 @@ class U2idx(object):
if icase and "casefold(" in q:
try:
v = unicode(v).casefold()
v = self.normfun(unicode(v))
except:
v = unicode(v).lower()

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import hashlib
@ -74,6 +74,7 @@ from .util import (
vsplit,
w8b64dec,
w8b64enc,
winsparse,
wunlink,
)
@ -1159,7 +1160,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@ -1431,6 +1432,8 @@ class Up2k(object):
rtop,
rei,
reh,
vol.flags["th_coversd"],
vol.flags["th_coversd_set"],
n4g,
ffat,
[],
@ -1508,6 +1511,8 @@ class Up2k(object):
rcdir: str,
rei: Optional[Pattern[str]],
reh: Optional[Pattern[str]],
th_cvd: list[str],
th_cvds: set[str],
n4g: bool,
ffat: bool,
seen: list[str],
@ -1533,8 +1538,6 @@ class Up2k(object):
cv = vcv = acv = ""
e_d = {}
th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set
scan_pr_s = self.args.scan_pr_s
assert self.pp and self.mem_cur # !rm
@ -1601,6 +1604,8 @@ class Up2k(object):
rap,
rei,
reh,
th_cvd,
th_cvds,
n4g,
fat32,
seen,
@ -1631,7 +1636,7 @@ class Up2k(object):
rsz += sz
files.append((sz, lmod, iname))
if sz:
if sz and th_cvd:
liname = iname.lower()
ext = liname.rsplit(".", 1)[-1]
if (
@ -4117,31 +4122,7 @@ class Up2k(object):
with self.rescan_cond:
self.rescan_cond.notify_all()
if rd and sz and fn.lower() in self.args.th_coversd_set:
# wasteful; db_add will re-index actual covers
# but that won't catch existing files
crd, cdn = rd.rsplit("/", 1) if "/" in rd else ("", rd)
try:
q = "select fn from cv where rd=? and dn=?"
db_cv = db.execute(q, (crd, cdn)).fetchone()[0]
db_lcv = db_cv.lower()
if db_lcv in self.args.th_coversd_set:
idx_db = self.args.th_coversd.index(db_lcv)
idx_fn = self.args.th_coversd.index(fn.lower())
add_cv = idx_fn < idx_db
else:
add_cv = True
except:
add_cv = True
if add_cv:
try:
db.execute("delete from cv where rd=? and dn=?", (crd, cdn))
db.execute("insert into cv values (?,?,?)", (crd, cdn, fn))
except:
pass
if "nodirsz" not in vflags:
if vflags and "nodirsz" not in vflags:
try:
q = "update ds set nf=nf+1, sz=sz+? where rd=?"
q2 = "insert into ds values(?,?,1)"
@ -4754,7 +4735,7 @@ class Up2k(object):
fsize,
ip,
time.time(),
None,
[dabs],
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
@ -4797,7 +4778,7 @@ class Up2k(object):
fsize,
ip,
time.time(),
None,
[sabs],
)
return "k"
@ -4927,7 +4908,7 @@ class Up2k(object):
fsize,
ip,
time.time(),
None,
[sabs],
)
return "k"
@ -5335,9 +5316,12 @@ class Up2k(object):
and self.args.sparse * 1024 * 1024 <= sz
):
try:
sp.check_call(["fsutil", "sparse", "setflag", abspath])
except:
self.log("could not sparse %r" % (abspath,), 3)
winsparse(f)
# some kernels/etc are buggy; force sync:
f.close()
f = open(abspath, "rb+")
except Exception as ex:
self.log("could not sparse %r: %s" % (abspath, ex), 3)
relabel = True
sprs = False
@ -5724,7 +5708,7 @@ def up2k_chunksize(filesize: int) -> int:
stepsize = 512 * 1024
while True:
for mul in [1, 2]:
nchunks = math.ceil(filesize * 1.0 / chunksize)
nchunks = -int(-filesize // chunksize)
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
return chunksize

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse
import base64
@ -429,6 +429,7 @@ IMPLICATIONS = [
["e2vu", "e2v"],
["e2vp", "e2v"],
["e2v", "e2d"],
["srch_nfkc", "srch_icase"],
["hardlink_only", "hardlink"],
["hardlink", "dedup"],
["tftpvv", "tftpv"],
@ -677,6 +678,12 @@ except:
HAVE_BWRAP = ""
if ANYWIN:
SCWD = os.environ["systemroot"]
else:
SCWD = None
def py_desc() -> str:
interp = platform.python_implementation()
py_ver = ".".join([str(x) for x in sys.version_info])
@ -1354,7 +1361,7 @@ class MTHash(object):
self.csz = chunksz
chunks: dict[int, tuple[str, int, int]] = {}
nchunks = int(math.ceil(fsz / chunksz))
nchunks = -int(-fsz // chunksz)
for nch in range(nchunks):
self.work_q.put(nch)
@ -2143,7 +2150,8 @@ class MultipartParser(object):
boundary = get_boundary(self.headers)
if boundary.startswith('"') and boundary.endswith('"'):
boundary = boundary[1:-1] # dillo uses quotes
self.log("boundary=%r" % (boundary,))
if len(boundary) > 72: # rfc-2046 <=70
raise Pebkac(400, "boundary 2big: %d" % (len(boundary),))
# spec says there might be junk before the first boundary,
# can't have the leading \r\n if that's not the case
@ -2947,6 +2955,25 @@ def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
raise
def trystat_shutil_move(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
try:
return shutil.move(src, dst)
except:
# ignore failed mtime on linux+ntfs; for example:
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
_, _, tb = sys.exc_info()
for _, _, fun, _ in traceback.extract_tb(tb):
if fun == "copystat":
if log:
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during move from %r to %r:\n%s"
log(t % (src, dst, min_ex()), 3)
os.unlink(src)
return dst # close enough
raise
def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool:
@ -2998,7 +3025,9 @@ def _fs_mvrm(
if not attempt and ex.errno == errno.EXDEV:
t = "using copy+delete (%s)\n %s\n %s"
log(t % (ex.strerror, src, dst))
osfun = shutil.move
if osfun in (os.replace, os.rename):
args.insert(0, log)
osfun = trystat_shutil_move
continue
if now - t0 > maxtime or attempt == 90209:
raise
@ -3035,7 +3064,7 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
os.unlink(bdst)
except:
pass
shutil.move(bsrc, bdst) # type: ignore
trystat_shutil_move(log, bsrc, bdst) # type: ignore
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
@ -4032,7 +4061,7 @@ def runihook(
t0 = time.time()
if fork:
Daemon(runcmd, cmd, bcmd, ka=sp_ka)
Daemon(runcmd, cmd, [bcmd], ka=sp_ka)
else:
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
if chk and rc:
@ -4198,6 +4227,10 @@ def _runhook(
if src in ("xm", "xban"):
ja["txt"] = txt[0]
ja["body"] = txt[1]
elif src == "xbr":
ja["ap_to"] = txt[0]
elif src in ("xar.ln", "xar.mv"):
ja["ap_from"] = txt[0]
else:
ja["wark"] = txt[0] # acshually the dwark but less confusing
if imp:
@ -4206,7 +4239,7 @@ def _runhook(
return mod.main(ja)
arg = json.dumps(ja)
else:
arg = txt[0] if txt else ap
arg = txt[0] if txt and src in ("xm", "xban") else ap
if acmd[0].startswith("zmq:"):
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
@ -4501,6 +4534,18 @@ def hidedir(dp) -> None:
pass
def winsparse(f: typing.BinaryIO) -> None:
assert ctypes # !rm
assert wk32 # !rm
import msvcrt
fh = msvcrt.get_osfhandle(f.fileno())
if not wk32.DeviceIoControl(
fh, 0x900C4, None, 0, None, 0, ctypes.byref(ctypes.c_ulong()), None
):
raise ctypes.WinError(ctypes.get_last_error())
_flocks = {}

View file

@ -121,6 +121,8 @@
<h2 id="wfp"><a href="{{ r }}/?h" id="goh">control-panel</a></h2>
<p id="js_bork">Looks like javascript is broken; see "csp nonce" section in readme</p>
<a href="#" id="repl">π</a>
</div>

View file

@ -46,6 +46,7 @@ if (1)
["ctrl-V", "paste (move/copy) here"],
["Y", "download selected"],
["F2", "rename selected"],
["F4", "update/refresh"],
"file-list-sel",
["space", "toggle file selection"],
@ -1248,6 +1249,9 @@ var img_re = APPLE ?
/\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i :
/\.(a?png|avif|bmp|gif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i;
var wopi_set = !window.have_wopi ? null :
new Set('odt fodt ott doc docx dotx rtf odm ods fods ots xls xlsx odp fodp otp ppt pptx ppsx odg fodg otg odf'.split(' '));
function set_files_html(html) {
var files = ebi('files');
@ -4058,6 +4062,11 @@ var fileman = (function () {
}
shui.style.display = 'block';
var wr = has(perms, 'write') && !fns.length;
var nope = ['admin', 'move', 'delete', 'upget'];
if (!wr)
nope.push('write');
var html = [
'<div>',
'<table>',
@ -4078,10 +4087,10 @@ var fileman = (function () {
'<tr><td>perms</td><td class="sh_axs">',
];
for (var a = 0; a < perms.length; a++)
if (!has(['admin', 'move', 'delete', 'upget'], perms[a]))
if (!has(nope, perms[a]))
html.push('<a href="#" class="tgl btn">' + perms[a] + '</a>');
if (has(perms, 'write'))
if (wr)
html.push('<a href="#" class="btn">write-only</a>');
html.push('</td></tr></div');
@ -6430,6 +6439,9 @@ var ahotkeys = function (e) {
if (k == 'F2')
return fileman.rename();
if (k == 'F4')
return treectl.goto();
if (!treectl.hidden && (!sh || !thegrid.en)) {
if (kl == 'a')
return QS('#twig').click();
@ -7681,6 +7693,11 @@ var treectl = (function () {
tn.href = addq(tn.href, 'v');
}
if (wopi_set && wopi_set.has(tn.ext))
tn.lead = '<a href="?wopi=' + bhref +
'" rel="nofollow" name="' + hname + '">📄</a>';
if (tn.lead == '-')
tn.lead = '<a href="?doc=' + bhref + '" id="t' + id +
'" rel="nofollow" class="doc' + (lang ? ' bri' : '') +
@ -7773,6 +7790,7 @@ var treectl = (function () {
r.hydrate = function () {
qsr('#bbsw');
qsr('#js_bork');
srvinf = ebi('srv_info').innerHTML.slice(6, -7);
if (ls0 === null) {
r.ls_cb = showfile.addlinks;
@ -8859,7 +8877,7 @@ var msel = (function () {
r.evsel = function (e, fun) {
ev(e);
r.so = r.pr = null;
var trs = QSA('#files tbody tr');
var trs = QSA('#files tbody tr' + (ebi('unsearch') ? ':not(.srch_hdr)' : ''));
for (var a = 0, aa = trs.length; a < aa; a++)
clmod(trs[a], 'sel', fun);
r.selui();

View file

@ -39,6 +39,7 @@ Ls.chi = {
["ctrl-V", "粘贴(移动/复制)到此处"],
["Y", "下载选中项"],
["F2", "重命名选中项"],
["F4", "刷新"], //m
"文件列表选择",
["space", "切换文件选择"],

View file

@ -39,6 +39,7 @@ Ls.cze = {
["ctrl-V", "vložit (přesunout/kopírovat) zde"],
["Y", "stáhnout vybrané"],
["F2", "přejmenovat vybrané"],
["F4", "obnovit"], //m
"výběr souborů",
["space", "přepnout výběr souboru"],

View file

@ -39,6 +39,7 @@ Ls.deu = {
["STRG-V", "Zwischenablage hier einfügen"],
["Y", "Auswahl herunterladen"],
["F2", "Auswahl umbenennen"],
["F4", "Aktualisieren"], //m
"file-list-sel",
["LEER", "Dateiauswahl aktivieren"],

View file

@ -39,6 +39,7 @@ Ls.epo = {
["stir-V", "alglui (movi/kopii) ĉi tien"],
["Y", "elŝuti elektitajn"],
["F2", "alinomi elektitajn"],
["F4", "refreŝigi"], //m
"file-list-sel",
["spacoklavo", "baskuli elektadon de dosieroj"],

View file

@ -39,6 +39,7 @@ Ls.fin = {
["ctrl-V", "siirrä tai kopioi tähän"],
["Y", "lataa valitut"],
["F2", "uudelleennimeä valitut"],
["F4", "päivitä"], //m
"file-list-sel",
["space", "vaihda tiedostonvalintatilaan"],

View file

@ -39,6 +39,7 @@ Ls.fra = {
["ctrl-V", "coller (déplacer/copier) ici"],
["Y", "télécharger la sélection"],
["F2", "renomer la sélection"],
["F4", "actualiser"], //m
"file-list-sel",
["Espace", "activer la sélection de fichiers"],

View file

@ -39,6 +39,7 @@ Ls.grc = {
["ctrl-V", "επικόλληση (μετακίνηση/αντιγραφή) εδώ"],
["Y", "λήψη επιλεγμένων"],
["F2", "μετονομασία επιλεγμένων"],
["F4", "ανανέωση"], //m
"λίστα αρχείων",
["space", "εναλλαγή επιλογής αρχείου"],

View file

@ -36,6 +36,7 @@ Ls.hun = {
['ctrl-V', 'beillesztés ide'],
['Y', 'kijelöltek letöltése'],
['F2', 'átnevezés'],
["F4", "frissítés"], //m
'kijelölés',
['space', 'fájl kijelölése'],

View file

@ -39,6 +39,7 @@ Ls.ita = {
["ctrl-V", "incolla (sposta/copia) qui"],
["Y", "scarica selezionati"],
["F2", "rinomina selezionati"],
["F4", "aggiorna"], //m
"file-list-sel",
["spazio", "alterna selezione file"],

View file

@ -39,6 +39,7 @@ Ls.jpn = {
["ctrl-V", "ここに貼り付け(移動/コピー)"],
["Y", "選択した項目をダウンロード"],
["F2", "選択した項目の名前を変更"],
["F4", "更新"], //m
"file-list-sel",
["space", "ファイル選択の切り替え"],

View file

@ -39,6 +39,7 @@ Ls.kor = {
["ctrl-V", "여기에 붙여넣기 (이동/복사)"],
["Y", "선택 항목 다운로드"],
["F2", "선택 항목 이름 바꾸기"],
["F4", "새로 고침"], //m
"파일 목록 선택",
["space", "파일 선택/해제"],

View file

@ -39,6 +39,7 @@ Ls.nld = {
["ctrl-V", "Hier plakken (verplaatsen/kopieëren)"],
["Y", "Download geselecteerde"],
["F2", "Hernoem geselecteerde"],
["F4", "vernieuwen"], //m
"bestand-lijst-selectie",
["space", "wissel bestand selectie"],

View file

@ -36,6 +36,7 @@ Ls.nno = {
["ctrl-V", "lim inn (flytt/kopiér)"],
["Y", "last ned valde"],
["F2", "endre namn på valde"],
["F4", "oppdater"],
"filmarkering",
["space", "markér fil"],

View file

@ -36,6 +36,7 @@ Ls.nor = {
["ctrl-V", "lim inn (flytt/kopiér)"],
["Y", "last ned valgte"],
["F2", "endre navn på valgte"],
["F4", "oppdater"],
"filmarkering",
["space", "marker fil"],

View file

@ -39,6 +39,7 @@ Ls.pol = {
["ctrl-V", "wklej (przenieś/skopiuj) tutaj"],
["Y", "pobierz zaznaczone"],
["F2", "zmień nazwę zaznaczonych"],
["F4", "odśwież"], //m
"file-list-sel",
["spacja", "przełącz zaznaczanie plików"],

View file

@ -39,6 +39,7 @@ Ls.por = {
["ctrl-V", "colar (mover/copiar) aqui"],
["Y", "baixar selecionado"],
["F2", "renomear selecionado"],
["F4", "atualizar"], //m
"seleção de lista de arquivos",
["espaço", "alternar seleção de arquivo"],

View file

@ -39,6 +39,7 @@ Ls.rus = {
["ctrl-V", "вставить (переместить/копировать) сюда"],
["Y", "скачать выделенное"],
["F2", "переименовать выделенное"],
["F4", "обновить"], //m
"выделение файлов",
["пробел", "выделить/снять выделение с текущего файла"],

View file

@ -39,6 +39,7 @@ Ls.spa = {
["ctrl-V", "pegar (mover/copiar) aquí"],
["Y", "descargar seleccionados"],
["F2", "renombrar seleccionados"],
["F4", "actualizar"], //m
"selección en lista de archivos",
["space", "alternar selección de archivo"],

View file

@ -39,6 +39,7 @@ Ls.swe = {
["ctrl-V", "klistra in (kopiera/flytta) hit"],
["Y", "ladda ner urval"],
["F2", "byt namn på urval"],
["F4", "uppdatera"], //m
"välja filer",
["Blanksteg", "växla val av fil"],

View file

@ -39,6 +39,7 @@ Ls.tur = {
["ctrl-V", "buraya yapıştır (taşı/kopyala)"],
["Y", "seçileni indir"],
["F2", "seçileni yeniden adlandır"],
["F4", "yenile"], //m
"dosya yönetimi seçimleri",
["boşluk", "seçimi değiştir"],

View file

@ -39,6 +39,7 @@ Ls.ukr = {
["ctrl-V", "вставити (перемістити/копіювати) сюди"],
["Y", "завантажити вибране"],
["F2", "перейменувати вибране"],
["F4", "оновити"], //m
"вибір файлів у списку",
["space", "перемкнути вибір файлу"],

View file

@ -36,6 +36,7 @@ Ls.vie = {
["ctrl-V", "dán (di chuyển/sao chép) tại đây"],
["Y", "tải xuống mục đã chọn"],
["F2", "đổi tên mục đã chọn"],
["F4", "làm mới"], //m
"file-list-sel",
["space", "chuyển đổi chọn tệp"],

View file

@ -998,6 +998,14 @@ function up2k_init(subtle) {
});
}
function unqueue_up(t) {
t.postlist = [];
var arr = st.todo.upload;
for (var a = arr.length - 1; a >= 0; a--)
if (arr[a].nfile == t.n)
arr.splice(a, 1);
}
var pvis = new U2pvis("bz", '#u2cards', uc, st),
donut = new Donut(uc, st);
@ -1069,25 +1077,28 @@ function up2k_init(subtle) {
return onovercmn(this, e, true);
}
function onovercmn(self, e, btn) {
var ok = false;
try {
var ok = false, dt = e.dataTransfer.types;
for (var a = 0; a < dt.length; a++)
var dt = e.dataTransfer.types;
for (var a = 0; a < dt.length && !ok; a++)
if (dt[a] == 'Files')
ok = true;
else if (dt[a] == 'text/uri-list') {
if (btn) {
for (var a = 0; a < dt.length && !ok; a++)
if (dt[a] == 'text/uri-list') {
if (btn)
ok = true;
if (toast.txt == L.u_uri)
toast.hide();
}
else
return toast.inf(10, L.u_uri) || true;
}
if (!ok)
return true;
if (toast.txt == L.u_uri && ok)
toast.hide();
}
catch (ex) { }
if (!ok)
return true;
ev(e);
try {
@ -2509,6 +2520,7 @@ function up2k_init(subtle) {
keepalive = t.keepalive,
me = Date.now();
while (apop(st.todo.handshake, t));
if (t.done)
return console.log('done; skip hs', t.name, t);
@ -2627,7 +2639,7 @@ function up2k_init(subtle) {
cbd.push(a == cdr_idx ? cdr_sz : chunksize);
}
t.postlist = [];
unqueue_up(t);
t.wark = response.wark;
var missing = response.hash;
for (var a = 0; a < missing.length; a++) {
@ -2916,8 +2928,7 @@ function up2k_init(subtle) {
var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText);
if (txt.indexOf('upload blocked by x') + 1) {
apop(st.busy.upload, upt);
for (var a = pcar; a <= pcdr; a++)
apop(t.postlist, a);
unqueue_up(t);
pvis.seth(t.n, 1, "ERROR");
pvis.seth(t.n, 2, txt.split(/\n/)[0]);
pvis.move(t.n, 'ng');
@ -2940,8 +2951,10 @@ function up2k_init(subtle) {
st.etac.u++;
st.etac.t++;
}
else if (txt.indexOf('already got that') + 1 ||
txt.indexOf('already being written') + 1) {
else if (txt.indexOf('already got that') + 1) {
unqueue_up(t);
}
else if (txt.indexOf('already being written') + 1) {
t.nojoin = t.nojoin || t.postlist.length;
console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t);
if (!toast.visible && st.todo.upload.length < 4)
@ -2949,6 +2962,10 @@ function up2k_init(subtle) {
}
else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
if (txt.indexOf('unknown wark') == 0) {
t.cooldown = t.coolmul = 0;
unqueue_up(t);
}
chill(t);
}
orz2(xhr);
@ -2959,8 +2976,10 @@ function up2k_init(subtle) {
apop(t.postlist, a);
if (!t.postlist.length) {
t.t_uploaded = Date.now();
pvis.seth(t.n, 1, 'verifying');
st.todo.handshake.unshift(t);
if (!has(st.busy.handshake, t)) {
pvis.seth(t.n, 1, 'verifying');
st.todo.handshake.unshift(t);
}
}
tasker();
}
@ -2995,6 +3014,7 @@ function up2k_init(subtle) {
if (!toast.visible)
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t);
unqueue_up(t); // maybe unknown wark (ff drops undrained rsp)
t.nojoin = t.nojoin || t.postlist.length; // maybe rproxy postsize limit
console.log('chunkpit onerror,', t.name, t);
orz2(xhr);

View file

@ -1199,8 +1199,10 @@ function has(haystack, needle) {
function apop(arr, v) {
var ofs = arr.indexOf(v);
if (ofs !== -1)
if (ofs !== -1) {
arr.splice(ofs, 1);
return true;
}
}
@ -1466,7 +1468,7 @@ function Debounce(delay) {
};
r.rm = function (fun) {
apop(r.q, fun);
while (apop(r.q, fun));
};
r.run = function () {
@ -1526,7 +1528,7 @@ var timer = (function () {
};
r.rm = function (fun) {
apop(r.q, fun);
while (apop(r.q, fun));
};
var doevents = function () {

42
copyparty/web/wopi.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en" id="ht_wopi">
<head>
<meta charset="utf-8"/>
<title>{{ title|e }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#{{ tcolor }}">
<style>
body,#w {
margin: 0;
padding: 0;
border: 0;
width: 100%;
height: 100%;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style></head><body>
<div style="display: none">
<form action="{{ url }}" enctype="multipart/form-data" method="post" target="w" id="submit-form">
<input name="access_token" value="{{ atoken }}" type="hidden" id="access-token"/>
<input name="access_token_ttl" value="{{ ttl }}000" type="hidden" id="access-token_ttl"/>
<input type="submit" value="" />
</form>
</div>
<iframe id="w" name="w" allow="clipboard-read *; clipboard-write *; fullscreen *"></iframe>
<script nonce="{{ js_nonce }}">
document.getElementById("submit-form").submit();
</script>
</body>
</html>

View file

@ -1,3 +1,78 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0727-2202 `v1.20.19` SECURITY: fix FTP upload outside volume
## ⚠️ ATTN: this release fixes a vulnerability in FTP and FTPS (not SFTP)
the FTP-server (default-disabled) would allow uploading to any folder that the copyparty process had permission to write to, but with certain limitations; see [GHSA-phv8-wgjp-g4p9](https://github.com/9001/copyparty/security/advisories/GHSA-phv8-wgjp-g4p9)
## recent important news
* [v1.20.19 (2026-07-27)](https://github.com/9001/copyparty/releases/tag/v1.20.19) fixed an FTP-server vuln (upload outside defined volumes)
* [v1.20.17 (2026-07-06)](https://github.com/9001/copyparty/releases/tag/v1.20.17) fixed a vuln when a volume has both filekeys and dirkeys enabled
* [v1.20.17 (2026-07-06)](https://github.com/9001/copyparty/releases/tag/v1.20.17) introduced csp nonces, possibly breaking some javascript-based plugins
## 🧪 new features
* #1495 wopi integration -- edit office documents in the web-ui (thx @brandon-doornbos!) d57bb0c7 10db4236
* currently only works with collabora online as wopi client #1574 and there's no docs/examples #1575
* thumbnails: use embedded cover-image in videos when available 59524018
* thumbnails: folder-thumbs can be disabled with `th-covers: no` (volflag or global) 856fada3
* #1555 macos: add [--srch-nfkc](https://copyparty.eu/cli/#g-srch-nfkc) to fix searching for filenames / paths in CJK languages a7c99094
* reduces search performance to around 30% when enabled
* hooks: `xbr` / `xar` did not include old/new abspath as parameters; now they do c122e103
## 🩹 bugfixes
* ftp: fix [GHSA-phv8-wgjp-g4p9](https://github.com/9001/copyparty/security/advisories/GHSA-phv8-wgjp-g4p9) b331bb1c
* #1563 moving files between volumes could fail depending on OS and underlying filesystem c70dc7ac
* fix drag-drop uploading in certain glitchy KDE environments (thx @tilse!) daf144af
* hooks: `xiu` crashed if the fork-flag was set (thx @stackxp!) aa862353
* hooks: `xau` without json-flag would be given the wark (file hash) instead of the abspath bae77b90
* shares: fix markdown-viewer (`?v`) inside shares 6a9437b7
* ftp: fix logging from `xbu` hooks 9912a951
* fix slow boot if a volume had lots of files in its toplevel folder cdb474c8
* python2.7: fix multithreaded file-hashing 0f2040c3
## 🔧 other changes
* #1556 the libvips thumbnailer was demoted to last-fallback due to frequently using excessive amounts of ram ed0be42a
* if [-lo](https://copyparty.eu/cli/#g-lo) points to an existing file, it will now be appended to instead of overwritten depending on [--rlo](https://copyparty.eu/cli/#g-help-rlo) b6abc33f
* #1530 nixos: the nix package now uses `ffmpeg-headless` instead of `ffmpeg-full` (thx @nyakase!) fface524
* slightly longer session cookie (was 20, now 24 chars) 537a99df
* Windows-specific:
* add a warning regarding the risks of DLL-hijacking when relevant bb40804f
* fix some trivial PATH-related footguns cea97ac6
* faster creation of sparse files bc45299b
* fix detection of filesystem characteristics 6226858b
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0709-2254 `v1.20.18` bwrap off
## 🧪 new features
* [shadowing](https://github.com/9001/copyparty/#shadowing): filesystem-path `//NULL` will fully unmap a volume from the server's filesystem (no more `/var/lib/empties/4` jank) 88846061
## 🩹 bugfixes
* fix a false-positive warning on startup 12878e6c1cbf4417da42d46230dff686dd48760b
## 🔧 other changes
* #1552 #1551 default-disabled the bwrap ffmpeg sandbox; 85a8cc99
* sandboxing can fail in too many creative ways (funky linux distros with funky filesystem layouts and policies) so autoconfiguring bwrap is not feasible
* to enable it, set `use-bwrap: y` after adjusting [th-bwrap](https://copyparty.eu/cli/#g-th-bwrap) to match your OS/env
* see the default `th-bwrap` value in `--help` on your server for a best-effort guess
* always rotate (never overwrite) `-lo` logfiles on startup unless `--rlo no` 29e145f5
* thumbnails: save a tiny bit of I/O by not refreshing expiration-times if `th-clean` (expiration) is disabled anyways e15f2927
## 🌠 fun facts
* no
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0706-1937 `v1.20.17` SECURITY: fix dirkeys

View file

@ -26,7 +26,7 @@ def up2k_chunksize(filesize):
stepsize = 512 * 1024
while True:
for mul in [1, 2]:
nchunks = math.ceil(filesize * 1.0 / chunksize)
nchunks = -int(-filesize // chunksize)
if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096):
return chunksize

View file

@ -1,6 +1,7 @@
# you can include additional config like this
# (the space after the % is important)
#
# specify either a single file or a folder with .conf files;
# since copyparty.d is a folder, it'll include all *.conf
# files inside (not recursively) in alphabetical order
# (not necessarily same as numerical/natural order)

View file

@ -49,7 +49,7 @@ with image size after installation and when gzipped
* [`min`](https://hub.docker.com/r/copyparty/min) (57 MiB, 20 gz) is just copyparty itself
* [`im`](https://hub.docker.com/r/copyparty/im) (70 MiB, 25 gz) can thumbnail images with pillow, parse media files with mutagen
* [`ac` (163 MiB, 56 gz)](https://hub.docker.com/r/copyparty/ac) is `im` plus ffmpeg for video/audio thumbs + audio transcoding + better tags
* [`iv`](https://hub.docker.com/r/copyparty/iv) (211 MiB, 73 gz) is `ac` plus vips for faster heif / avic / jxl thumbnails
* [`iv`](https://hub.docker.com/r/copyparty/iv) (211 MiB, 73 gz) is `ac` plus vips for more thumbnail formats
* [`dj`](https://hub.docker.com/r/copyparty/dj) (309 MiB, 104 gz) is `iv` plus beatroot/keyfinder to detect musical keys and bpm
[`ac` is recommended](https://hub.docker.com/r/copyparty/ac) since the additional features available in `iv` and `dj` are rarely useful

View file

@ -15,7 +15,7 @@ command -v gsort >/dev/null && {
cd ../dist
kwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -ls -dbd -chmod -pwhash -zm'
kwds='-bind -accounts -auth -auth-ord -flags -handlers -hooks -idp -urlform -exp -rlo -ls -dbd -chmod -pwhash -zm'
export PRTY_FULL_HELP=1
export PRTY_NO_PARAMIKO=1

View file

@ -58,6 +58,7 @@ rm -rf mods/magic/
sed -ri '/(bccache|PackageLoader)/d' mods/jinja2/__init__.py
af '/^class/{s=0}/^class PackageLoader/{s=1}!s' mods/jinja2/loaders.py
sed -ri 's/from url.*Request, urlopen.*/pass/' mods/copyparty/svchub.py
sed -ri 's/from urllib.* import urlopen.*/pass/' mods/copyparty/httpcli.py
sed -ri 's/(.*"--vc-.*, help=).*/\1argparse.SUPPRESS)/' mods/copyparty/__main__.py
}
[ $w10 ] && {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -141,3 +141,4 @@ copyparty/web/tl/vie.js,
copyparty/web/ui.css,
copyparty/web/up2k.js,
copyparty/web/util.js,
copyparty/web/wopi.html,

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: latin-1
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
import subprocess as sp

View file

@ -1,5 +1,5 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import sys

View file

@ -71,6 +71,7 @@ Ls.hmn = {
["ctrl-V", "paste (move/copy) here"],
["Y", "download selected"],
["F2", "rename selected"],
["F4", "update/refresh"],
"file-list-sel",
["space", "toggle file selection"],

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import io
import os

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import shutil

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import json
import os

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import io
import json

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import re
import unittest

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import shutil

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import io
import os

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import json
import os

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import os
import re

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import json
import os

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import json
import os

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import unittest

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import json
import os

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