Compare commits

...
209 Commits
Author SHA1 Message Date
ed 7ccebb6948 v1.20.26 2026-10-14 23:21:55 +00:00
ed ed0fcd608f th: fix .raw if not pil jxl 2026-10-14 21:55:12 +00:00
ed 9811cd3101 thx faelnor 2026-10-14 21:54:58 +00:00
ed fa8913c712 dumb 2026-10-14 21:53:36 +00:00
ed 4bd64d7456 shadowing: less false-positives 2026-10-14 21:51:52 +00:00
ed 7ce483e970 xvol: ls filter 2026-10-14 21:50:40 +00:00
ed 82eab3d2f8 xvol/xdev (GHSA-hg99-jqfp-rf76) 2026-10-14 21:49:04 +00:00
ed 0dc1cb3291 docker: drop openjpeg 2026-10-09 00:26:10 +00:00
ed 1a87dc3218 update pkgs to 1.20.25 2026-10-06 00:44:54 +00:00
ed dc3adc0f4d v1.20.25 2026-10-06 00:35:29 +00:00
ed 5f00c1631c return to ?b=u; closes #1659 2026-10-06 00:33:31 +00:00
ed e59fc05e73 add --sftp-chattr 2026-10-05 23:47:51 +00:00
ed e8de421174 shadowing + casefolding (GHSA-3c88-p299-7hvr) 2026-10-05 23:46:39 +00:00
ed 36aaa24974 file-rename instant preview on open 2026-10-05 23:02:30 +00:00
ed 5e0a88e355 ban-pw basic-auth; closes #1660 2026-10-05 22:36:30 +00:00
ed 4b283b4bfd bbox: fix #1650 (el null) 2026-10-05 22:32:49 +00:00
ed 64f574fe94 u2c: allow \ in filenames 2026-10-05 22:26:18 +00:00
Michael Rosenberg a31870cbd3 dk/fk constant-time (#1658) 2026-10-05 22:01:30 +00:00
ed 74b17374c2 DELETE: say 404 on enoent; closes #1656 2026-10-03 22:10:17 +00:00
ed 670e86228c tests: support new coreutils or whatever 2026-10-03 22:08:10 +00:00
ed 1341b3421f --smb now requres --hack-me-bro 2026-10-03 22:01:49 +00:00
ed 5961228c93 deadcode 2026-10-03 21:44:14 +00:00
ed 28f9f6f002 allow \ in filenames on linux/macos;
now possible to upload 09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac
2026-10-03 21:43:47 +00:00
ed 186cbf939f sftp: add fastloader;
startup on an 800mhz armv5 exceeded the 30sec timeout
2026-10-02 03:29:12 +00:00
ed 080935788b redup: skip conclusive hard/ref; closes #1652 2026-09-30 00:05:02 +00:00
ed c6918c2ce6 tests: macos 2026-09-30 00:15:08 +02:00
ed ab2299e3db add acao-re; closes #1498 2026-09-27 19:12:01 +00:00
edandTil Schmitter 334dccc55e --ih: fix nav to webroot
Co-authored-by: Til Schmitter <til.schmitter@gmail.com>
2026-09-27 18:23:08 +00:00
ed 8d7aa4f860 cosmetic 2026-09-27 18:20:47 +00:00
ed f73b37c6fa pipe-dl progress on /?h 2026-09-27 18:11:29 +00:00
ed 731dbbff84 u2c: fix exfat's http502 allergy;
addendum to 23e00c0a04
2026-09-26 23:43:26 +00:00
ed 23e00c0a04 up2k-js: fix exfat's http502 allergy;
normally, to avoid excessive handshaking, unexpected server errors are
handled by continuing onto the next chunk and dealing with it at eof

this doesn't work on filesystems that dont support sparse files, so
discard the chunk-queue and re-handshake to keep it sequential
2026-09-26 22:12:15 +00:00
ed 107dec9ed1 ifaddr: winxp fix 2026-09-24 19:31:21 +00:00
willie6666 f445bfa862 shares: delete sf entries too (#1647)
Signed-off-by: willie6666 <89621369+willie6666@users.noreply.github.com>
2026-09-20 13:12:56 +00:00
ed aff083fb28 update pkgs to 1.20.24 2026-09-19 21:39:09 +00:00
ed 5ccc999f39 v1.20.24 2026-09-19 21:33:56 +00:00
ed 16a3f5d51a add --help-redup + failsafe 2026-09-19 20:48:28 +00:00
ed 35af7c9fef js: add msel fn 2026-09-19 19:14:06 +00:00
ed 4d1f2a9301 js: fix contrived crash (closes #1516);
view textfile, sel, delete, sel, boom
2026-09-19 19:13:41 +00:00
ed 229159027b add --help-dedup 2026-09-19 17:43:51 +00:00
ed ec4a95e1a5 up2k nosub; closes #1611 2026-09-19 17:42:39 +00:00
ed eb4f1dddce lint 2026-09-18 20:59:16 +00:00
Mica Whiteanded 097b83056e textfile-viewer: parse shebang (#1643)
detect filetype based on #!/interpreter

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-09-18 19:47:09 +00:00
ed c688bb2e46 xiu: add trailing newline 2026-09-17 22:00:24 +00:00
ed c92435c2d2 add -i no 2026-09-17 21:53:50 +00:00
ed 22afaa7586 nocow histpath iff btrfs 2026-09-17 21:46:12 +00:00
ed 5ddbce31fc xlink-reflink only same-fs;
avoid waking other disks which wont work anyways
2026-09-17 21:24:10 +00:00
ed ec8ec0f1a6 un-cargocult bools 2026-09-17 21:12:46 +00:00
ed 558f6aee44 shallowclone up2k.flags;
update flags on config-reload while keeping up2k coherent
2026-09-17 19:56:58 +00:00
ed 6867ccc511 fix PRTY_NO_IFADDR + cleanup 2026-09-16 22:38:23 +00:00
ed 947d73eeb6 check xlink on copy 2026-09-16 22:35:11 +00:00
ed 735d6e00de up2k: reflink is xlink-safe 2026-09-16 22:34:19 +00:00
ed fd4f434192 add --redup 2026-09-16 22:33:38 +00:00
ed 971a62cc89 up2k: iOS: reduce jitter;
* xhr.abort() to close/free tcp
* onfoo=null fixes done-flicker
2026-09-15 19:29:47 +00:00
ed bfc3cff848 unsanctioned: sourceforge 2026-09-08 06:01:30 +02:00
ed 8967726f61 update pkgs to 1.20.23 2026-09-06 22:53:14 +00:00
ed ad9994bef3 v1.20.23 2026-09-06 22:49:08 +00:00
ed e5e2e97910 archlinux: add post_upgrade 2026-09-06 22:08:42 +00:00
ed e42e01563e update pkgs to 1.20.22 2026-09-06 21:18:21 +00:00
ed de21c210c6 v1.20.22 2026-09-06 21:12:32 +00:00
ed 55969b8e54 dk: block subdir zip (GHSA-mc69-pxc8-4xf4) 2026-09-06 21:07:39 +00:00
ed b80a210e0f sftp: xvol/xdev (GHSA-3fhv-rhjw-7hrg) 2026-09-06 21:07:21 +00:00
ed 8016512064 sfx: link explanation 2026-09-06 18:22:52 +00:00
ed 72eb03bf43 exe: drop license filter 2026-09-06 15:02:32 +00:00
ed f870cf3c05 sfx: prefer ~/.cache/; closes #1517 2026-09-05 22:49:01 +00:00
ed e6c9c6fbeb sfx: golf 2026-09-05 22:43:13 +00:00
ed dd9cf6456d btrfs: nocow .hist for sqlite perf 2026-09-05 20:54:37 +00:00
ed 185087ee05 systemd: use /etc/copyparty.conf; closes #1631 2026-09-05 17:56:19 +00:00
ed 3e3401f4ed allow dk download-as-zip 2026-09-05 12:55:58 +00:00
ed a78611d8c5 dont trust zip headers 2026-09-05 11:29:50 +00:00
ed 370a05ab3c add --no-mime / volflag nomime 2026-09-05 11:00:40 +00:00
ed d7d30f268e connect: rclone obscure; closes #1632 2026-09-04 19:14:06 +00:00
ed 34229337cc the font fell off 2026-09-04 19:01:44 +00:00
ed 0767d95674 up2k: fix !e2d reload blocking uploads 2026-09-04 18:51:15 +00:00
ed f00d19c5d3 avoid binary garbage in logs;
also stricten RE_HTTP1 against false-positives
2026-09-04 18:21:20 +00:00
ed ab791921ea fix csp-nonce on chrome<62 2026-09-04 17:54:20 +00:00
ed 2cebd8ce16 i18n: 2old 2026-09-04 17:54:04 +00:00
ed 662e3c5d59 indicate F4 instantly 2026-09-04 16:08:35 +00:00
ed 3ea671ff9a iOS Safari XHR has gone deaf;
workaround for iPhone bug in
iOS 18.6.2, 26.6, 26.6.1, ...

safari devtools has the full response,
it just never bothers calling xhr.onload
and xhr.readystate is unusably racey
(final value is any of 1..3)
2026-09-02 22:21:07 +00:00
ed f792a9529c add up2k mutex ping 2026-09-02 20:23:54 +00:00
ed 4e318d5165 deadcode 2026-09-01 23:02:48 +02:00
ed 80cf7bfdc0 certs: conditional whining 2026-09-01 23:00:10 +02:00
ed 704af697c7 packaging: license: rm missing/unvendored 2026-09-01 18:46:59 +02:00
ed b854b8f596 packaging: *.gif + pre-gzip js/css;
* web/*.gif was ignored by pyproject.toml
* pre-gzip js/css in dist source tarball
* systemd/archlinux: safer default-config
* readme: "config file example" labeling
2026-09-01 14:50:57 +02:00
Danila Kamaev 936e8a3bf3 rcm: wopi (#1607) 2026-08-31 16:36:55 +02:00
yuki 7796c2d06a add jxl support for autogrid (#1610) 2026-08-31 16:24:04 +02:00
Danila Kamaev adf5f639fa translation: rus review (#1612) 2026-08-31 16:22:46 +02:00
Danila Kamaev d4a1ce48e4 thumbex: collabora example (#1617) 2026-08-31 16:19:17 +02:00
Danila Kamaev 0a957bf264 thumbex: update example link (#1629) 2026-08-31 16:14:14 +02:00
ed bdf8b793d9 http206: fix footer (last-n-bytes) reqs 2026-08-31 16:08:05 +02:00
ed 69c022f565 misc 2026-08-31 15:41:01 +02:00
ed 7984ef9232 tests: fix macos 2026-08-31 15:40:21 +02:00
ed 9de090265f hooks: phonecam-sorter.py;
written 2025-04-27, just never left the nas
2026-08-23 22:28:45 +00:00
ed 6115eb452d update pkgs to 1.20.21 2026-08-17 22:05:44 +00:00
ed 32eadc0f14 v1.20.21 2026-08-17 21:57:27 +00:00
ed 566de65f17 option to allow js in svg 2026-08-17 21:04:45 +00:00
ed 78a4ee49a2 wopi-accs + support unmapped root 2026-08-17 19:40:22 +00:00
ed 9ff6a71a2a shares: hide md-hist too 2026-08-16 22:31:09 +00:00
ed 21c2c72889 shares: harden single-file shares some more;
motivated by a bug-report that couldn't be reproduced and
also didn't make sense, a "hallucination" as they call it?
(was submitted by an autonomous bot or something, man)

so this should make no difference aside from removing footguns
2026-08-16 22:22:51 +00:00
ed 18791c5768 up2k: on tcp loss, await !busy.up b4 re-hs;
avoids parallel reschedule of inflight chunks;
http400: that chunk is already being written to:
2026-08-16 20:48:48 +00:00
ed 2be85b67a9 lint 2026-08-16 18:23:12 +00:00
ed eeb399e3c9 fix dupe logue on !js hydrate 2026-08-16 18:20:11 +00:00
ed dcc0abd696 dsel: disable on bbox 2026-08-15 21:40:04 +00:00
ed 48d9de03fe whitespace 2026-08-15 21:38:51 +00:00
ed dbc6df812b th: add extractor example 2026-08-15 20:11:24 +00:00
Danila Kamaevanded 1864805072 th: custom thumbnail extractors (#1602)
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-08-15 19:54:47 +00:00
shermanhlc 47297475bf u2c: password in U2C_PW (#1604) 2026-08-15 13:51:09 +00:00
ed 3098cf59cb wopi stuff:
* open in new tab
* print wopi url on error
* fix helptext to <= 15 ch
2026-08-15 13:25:36 +00:00
Danila Kamaev efcf96e3e3 wopi: collaborate (#1591)
persistent file IDs for real-time collaboration
2026-08-15 13:06:45 +00:00
Danila Kamaev 92c3f32e13 wopi: fix access_token_ttl (#1587) 2026-08-15 12:47:04 +00:00
ed 14e2d79b08 ignore PRTY_CONFIG if autodiscovered to prevent dupe vols 2026-08-15 09:56:13 +00:00
ed 825f1c9456 bbox: open svg; closes #1605 2026-08-15 09:48:22 +00:00
ed f19ef033ec up2k: u2sz: allow 1 as max + detect http413 2026-08-15 09:45:13 +00:00
ed 8b6e8972fe copyparty.exe: python 3.14 2026-08-12 18:38:53 +00:00
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
edandVictor Phillips 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 Doornbosanded 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
edandTil Schmitter 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
ed e700e4c421 v1.20.18 2026-07-09 22:54:41 +00:00
ed e15f292766 th: no keepalive-pokes if th-clean 0 2026-07-09 22:16:11 +00:00
ed 8884606138 shadowing: //NULL as unmap keyword 2026-07-09 21:42:59 +00:00
ed 12878e6c1c show_hist: shadowing gave false-positive 2026-07-09 20:16:29 +00:00
ed 29e145f5e4 rlo always unless no or blank 2026-07-09 19:07:18 +00:00
ed 85a8cc99ba bwrap: default-disable;
many distros dont care about FHS, so
th-bwrap cannot be autoconfigured
2026-07-09 18:56:25 +00:00
ed a523ba231f update pkgs to 1.20.17 2026-07-06 19:46:00 +00:00
ed f0feab0889 v1.20.17 2026-07-06 19:37:07 +00:00
ed e40755331b fix GHSA-x5pq-m9p8-f4vx; no dk with fk 2026-07-06 17:00:23 +00:00
ed 89040874f5 shr: remove upget (serves no purpose) 2026-07-06 16:17:38 +00:00
ed 9fa950bcfa plaintext readme/logue volflags 2026-07-06 16:11:20 +00:00
ed a3fbba10f7 genhelp: FULL_HELP 2026-07-06 15:51:14 +00:00
ed 741b00f342 fix tests 2026-07-06 10:18:29 +00:00
ed 2c8325a770 show-hist: nested volumes notice 2026-07-06 10:18:02 +00:00
ed db68353e22 docker blocks bwrap 2026-07-06 08:55:20 +00:00
ed 68568b7743 exe: deps: update pillow 2026-07-05 20:59:31 +00:00
ed 3888048755 windows fix; closes #1543 2026-07-05 19:29:32 +00:00
ed 379c0aa6b4 volflags: no_readme, no_logues 2026-07-05 19:28:51 +00:00
ed 8c4e931300 sftp: filter spam from portscanners (closes #1519);
also use a threadpool for accepting clients, to avoid
spinning up a new thread for each hit from a portscanner
2026-07-05 18:59:09 +00:00
ed 32d074ffc5 docker: version/ctime labels; closes #1532 2026-07-05 16:08:01 +00:00
ed 85be3b8de2 detect bwrap init-fail; prisonparty et al 2026-07-04 21:46:41 +00:00
ed 7b21e9f9ce r 2026-07-04 21:41:12 +00:00
suhas 56b6db0875 contrib: traefik + docker (#1511)
* add traefik compose file
* add copyparty compose file w/ traefik

Signed-off-by: suhas <hi@suhas.one>
2026-07-04 09:33:18 +00:00
Mobin fe006073a9 docs: s6-notify (#1500)
The way to use the S6_NOTIFY_FD is different between dinit and s6, but the README only mentioned dinit syntax with a link to the s6 documentation
2026-07-04 09:27:16 +00:00
doskelanded 8242e69359 ftp: banner (#1504)
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-07-04 09:25:29 +00:00
ed 34c856e838 md-edit: md_nhist (max num backups) 2026-07-02 20:34:48 +00:00
ed fa1499ae15 md-edit: hide .hist by default 2026-07-02 20:20:24 +00:00
ed be31a74485 sqlite: make corruption more obvious 2026-07-02 16:24:26 +00:00
Damian Romanowskianded 12d877b0d0 bbox: cbz: go-to-page (#1535)
add "go-to-page" button + page-number in url

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2026-07-02 16:17:54 +00:00
ed d3b9599421 csp script nonce 2026-06-29 19:26:01 +00:00
ed 9a7f474750 deps: orbitron 2026-06-28 19:38:04 +00:00
ed 52c42b83e4 optimize md-edit 2026-06-28 15:52:46 +00:00
ed 4c8203016f docker: ffmpeg: chipping at the iceberg 2026-06-26 16:55:33 +00:00
ed a4f259dafb docker: ffmpeg: feature list 2026-06-26 16:06:10 +00:00
NecRaul d33d11321f fix directory sort-order (closes #1119) (#1528);
folders sorted incorrectly due to the trailing slash,
for example `a/` vs. `a b/`
2026-06-16 22:25:08 +00:00
NecRaul e017b1bc6e fix(rotf): use global rotf_tz as volume fallback 2026-06-16 20:40:37 +02:00
ed efa43f891b ffmpeg bwrap sandbox 2026-06-13 21:26:09 +00:00
ed 90639de984 u2c: hash stdin to wark 2026-06-13 11:47:47 +00:00
ed 5dbff4af41 js: mkdir: cd if existed; closes #1512 2026-06-13 11:45:21 +00:00
ed 5beecd6622 fix xbu reloc resume;
both up2k.js and u2c.py would ignore the "partial upload exists"
assuming it was self-inflicted, skipping the file
2026-06-13 11:20:13 +00:00
ed 5a8949d595 vjoins 2026-06-13 11:15:20 +00:00
ed 22c3a3dd46 pop job.vcfg; fixes up2k._snap_reg 2026-06-13 10:39:26 +00:00
ed a00bc93fe1 bsd: signal masking 2026-06-12 23:37:12 +00:00
ed a48afe87b9 docker: alpine 3.24 2026-06-12 21:47:04 +00:00
ed c3eb9ecec2 docs: docker archs 2026-06-12 21:29:35 +00:00
ed 6e75faa623 docker: drop arm32 iv;
deadlock on startup, probably bad cffi
2026-05-26 19:16:31 +00:00
ed 06af60276b update pkgs to 1.20.16 2026-05-26 18:49:07 +00:00
171 changed files with 5466 additions and 1416 deletions

No files matched your search

+1
View File
@@ -4,4 +4,5 @@
*.png binary
*.gif binary
*.ico binary
*.gz binary
+94 -16
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
@@ -83,6 +84,7 @@ built in Norway 🇳🇴 with contributions from [not-norway](https://github.com
* [exclude-patterns](#exclude-patterns) - to save some time
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
* [periodic rescan](#periodic-rescan) - filesystem monitoring
* [u2sz](#u2sz) - configure upload-chunking
* [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
@@ -136,6 +138,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
@@ -216,6 +219,8 @@ other places to download copyparty from (non-github links):
* https://copyparty.eu/enz = https://copyparty.eu/copyparty-en.pyz = the enterprise pyz
* https://copyparty.eu/cli = online cli helptext
DO NOT download from [sourceforge](https://copyparty.eu/unsanctioned/sourceforge.txt)
### at home
@@ -245,7 +250,7 @@ you may also want these, especially on servers:
because the following environment variables are commonly used in service-scripts, they are understood by copyparty:
* `NOTIFY_SOCKET` as provided by systemd with service type=notify (see systemd/copyparty.service above)
* `S6_NOTIFY_FD` for s6/dinit [`ready-notification = pipevar:S6_NOTIFY_FD`](https://skarnet.org/software/s6/notifywhenup.html)
* `S6_NOTIFY_FD` for dinit [`ready-notification = pipevar:S6_NOTIFY_FD`](https://davmac.org/projects/dinit/man-pages-html/dinit-service.5.html#ready) and [s6 supervision suite notification](https://skarnet.org/software/s6/notifywhenup.html) support
and remember to open the ports you want; here's a complete example including every feature copyparty has to offer:
```
@@ -523,6 +528,7 @@ upgrade notes
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using 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
* when using config-files, ignore all the `-a` and `-v` stuff below, and CTRL-F `config file example` on this page instead
a quick summary can be seen using [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)
@@ -566,12 +572,14 @@ if you want to grant access to all users who are logged in, the group `acct` wil
* to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
* single users can also be subtracted from a group: `@admins,-james`
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
anyone trying to bruteforce a password gets banned according to `--ban-pw` (assuming [real-ip](#real-ip) is configured correctly); default is 24h ban for 9 failed attempts in 1 hour
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
* you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc)
config file example:
```yaml
[accounts]
u1: p1 # create account "u1" with password "p1"
@@ -614,14 +622,30 @@ and if you want to use config files instead of commandline args (good!) then her
hiding specific subfolders by mounting another volume on top of them
> **WARNING:** shadowing is NOT bulletproof when the underlying filesystem is casefolding (case-insensitive), in other words DON'T rely on this when copyparty is running on Windows and/or MacOS. This is also unsafe when combined with certain filesystems on Linux, especially FAT32/exFAT. Pay attention to the volume-listing in the startup log; `casechk:n` indicates probably-OK, but if it says `casechk:y` then DO NOT trust shadowing to fully prevent access to a file/subfolder.
for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
to fully unmap it from the filesystem, specify `//NULL` instead of a real path such as `/var/empty`, so for example `-v /mnt::r -v //NULL:web/certs:` ensures `/web/certs` will never be accessible by anyone
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
so, to shadow a file/folder, define a volume but leave out the `accs:` section
> ℹ️ this also works for single files, because files can also be volumes
config file example for unmapping folders by shadowing:
```yaml
[/drives]
/mnt # url "/drives" goes to "/mnt"
accs:
r: * # everyone can read
[/drives/foo/bar]
//NULL # blocks access to "/mnt/foo/bar"
```
## dotfiles
@@ -759,7 +783,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)
@@ -768,6 +792,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
@@ -905,7 +930,7 @@ note that since up2k has to read each file twice, `[🎈] bup` can *theoreticall
if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option `--u2sz` (the default is 96 MiB to support Cloudflare)
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option [--u2sz](#u2sz)
if you want to replace existing files on the server with new uploads by default, run with `--u2ow 2` (only works if users have the delete-permission, and can still be disabled with `🛡️` in the UI)
@@ -1323,7 +1348,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
@@ -1615,6 +1641,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
@@ -1770,12 +1805,17 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access, keeping permissions from outer/initial volume
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access
* usually keeps permissions from outer/initial volume, but don't take that for granted
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
these options will reduce performance; unlikely worst-case estimates are 27% reduction for directory listings, 35% for download-as-tar
* if the underlying storage is [a network disk](#using-the-cloud-as-storage) then xvol/xdev will be **very** slow (not to mention pointless/useless)
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
> **WARNING:** the `xvol` and `xdev` options should NOT be relied on for security; these are originally and primarily intended for the [e2ds indexer](#file-indexing). Certain underlying filesystems (especially FUSE-based and casefolding ones) will be problematic, and symlinks inherently have various TOCTOUs, and there's probably also bugs in copyparty where xvol/xdev is not fully enforced for certain APIs. The developer does not care about these options and they do not receive much testing.
### periodic rescan
filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date
@@ -1799,6 +1839,23 @@ config file example:
```
## u2sz
configure upload-chunking; the default `64,96` is optimal for nearly every scenario, however:
* if you are seeing the message `bad server config; see "u2sz" in readme` then you have a proxy/CDN/whatever with a `max-request-body-size` which is too small, and you must either increase the limit in the reverseproxy config to at least 96 MiB (recommended) or reduce the target chunksize in copyparty (continue reading)
the global-option `--u2sz` is two numbers, `target,max` MiB of each chunk to send, aiming for `target` MiB and allowing up-to-and-including `max` MiB [depending on filesize](./docs/devnotes.md#list-of-chunk-sizes), and if you only specify one number then it sets that for both
cloudflare max is 96 MiB, but if you have a reverseproxy (or CDN/WAF/whatever) which is more restrictive, then try some of the following:
* `u2sz: 32` is still acceptable performance in most cases
* `u2sz: 8` is the lowest recommended value; anything lower will reduce performance over long physical distances and/or shitty routes
* `u2sz: 1` is the absolute minimum, and will be slow in many cases
the setting can also be changed in the web-UI; the textbox next to `date-chk` in the settings tab
## upload rules
set upload rules using volflags, some examples:
@@ -2150,7 +2207,7 @@ as for client-side stuff, there is [plugins for modifying UI/UX](./contrib/plugi
autologin based on IP range (CIDR) , using the global-option `--ipu`
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, or put this in a config file:
for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, same as this config file example:
```yaml
[global]
@@ -2166,7 +2223,7 @@ repeat the option to map additional subnets
limit a user to certain IP ranges (CIDR) , using the global-option `--ipr`
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file:
for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, same as this config file example:
```yaml
[global]
@@ -2203,7 +2260,7 @@ but if you just want to let users change their own passwords, then you probably
other ways to auth by header
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file:
if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file example:
```yaml
[global]
@@ -2524,7 +2581,7 @@ change the association of a file extension
using commandline args, you can do something like `--mime gif=image/jif` and `--mime ts=text/x.typescript` (can be specified multiple times)
in a config file, this is the same as:
that's the same as this config file example:
```yaml
[global]
@@ -2561,6 +2618,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 |
@@ -2590,7 +2648,7 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty.conf` or `~/.config/copyparty/copyparty.conf`
after installing, start either the system service or the user service and navigate to http://127.0.0.1:3923 for further instructions (unless you already edited the config files, in which case you are good to go, probably)
@@ -2636,8 +2694,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
@@ -2998,7 +3054,7 @@ below are some tweaks roughly ordered by usefulness:
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
* if you are running the copyparty server **on Windows or Macos:**
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* `--casechk=n` makes it much faster, but [reduces security](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr) and awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* this is the same as `casechk: n` in a config-file
@@ -3036,6 +3092,9 @@ some notes on hardening
* the `nohtml` volflag also enables `noscript` which, on its own, prevents *most* javascript from running; enabling just `noscript` without `nohtml` makes it probably-safe (see below) to view html and svg files, but `nohtml` is necessary to block javascript in markdown documents
* "probably-safe" because it relies on `Content-Security-Policy` so it depends on the reverseproxy to forward it, and the browser to understand it, but `nohtml` (the nuclear option) always works
* when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or [`--help-bind`](https://copyparty.eu/cli/#bind-help-page)
* put ffmpeg into a sandbox (helps against future ffmpeg vulns) by configuring [--th-bwrap](https://copyparty.eu/cli/#g-th-bwrap) and enable it with `--use-bwrap y`
* usually impossible when running in docker or podman
* the default value of `--th-bwrap` (see `--help` on your own server) is autogenerated best-effort guess based on your setup; installing `bwrap` and configuring this is up to you (good luck)
safety profiles:
@@ -3079,6 +3138,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
@@ -3105,6 +3181,8 @@ permissions `wG` (write + upget) lets users upload files and receive their own f
share specific folders in a volume without giving away full read-access to the rest -- the visitor only needs the `g` (get) permission to view the link
> ℹ️ a better alternative to this is [shares](#shares)
volflag `dk` generates dirkeys (per-directory accesskeys) for all folders, granting read-access to that folder; by default only that folder itself, no subfolders
volflag `dky` disables the actual key-check, meaning anyone can see the contents of a folder where they have `g` access, but not its subdirectories
@@ -3220,7 +3298,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]"`
+1
View File
@@ -78,6 +78,7 @@ cd /mnt/nas/music/.hist
# [`prisonparty.sh`](prisonparty.sh)
* run copyparty in a chroot, preventing any accidental file access
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
* **not compatible with:** `--th-bwrap`
# [`bubbleparty.sh`](bubbleparty.sh)
* run copyparty in an isolated process, preventing any accidental file access and more
+1
View File
@@ -36,6 +36,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
* [phonecam-sorter.py](phonecam-sorter.py) is for syncing backups of phone gallery into year/month folders
# more upload stuff
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import os
import re
import sys
_ = r"""
purpose:
sync/backup phone gallery (pics/vids) to a nas which runs copyparty
what this hook does:
redirect photos/videos into date-based folders, named "%Y%m" according
to filename; that is, "/cam/sort/DCIM/D027/PXL_20250828_202820075.jpg"
will be redirected to "/cam/pics/ts/202508/PXL_20250828_202820075.jpg"
requires this setup on the server:
* /cam/ is a volume with all your pics/vids
* the phone must upload to somewhere below /cam/sort/
* photo/video filenames match the following regex:
^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$
* so for example these filenames are OK:
* PXL_20250828_202820075.jpg
* IMG_20251123_202405.jpg
* PANO_20180421_121245_03.t8o5.jpg
* VID_20250307_220250-crf34.mp4
example server config as a volflag:
-v /mnt/thedisk/gallery:cam:rwa,ed:c,e2ds,xbu=I,~/hooks/phonecam-sorter.py
example server config in a copyparty config file:
[/cam]
/mnt/thedisk/gallery
accs:
rwa: ed
flags:
e2ds
xbu: I,~/hooks/phonecam-sorter.py
command to run on phone to sync to server, sending new files only:
~/bin/u2c.py -te homenas.ca -a '$homenas.pw' https://10.0.0.2:3923/cam/sort/ ~/storage/dcim/Camera
the phone-command above assumes this setup on the phone:
* termux or another linux-like thing with access to gallery files
* homenas.ca is a textfile with the server's CA certificate
* homenas.pw is a textfile with the copyparty password inside
* ~/storage/dcim/ exists; if not, run termux-setup-storage
* ~/bin/u2c.py exists; if not, curl -LO https://10.0.0.2:3923/.cpr/a/u2c.py && chmod 755 u2c.py
unfortunate consequences:
* due to the redirection, u2c -z doesn't do anything, so every local
file is hashed every time, wasting some time and cpu on the phone
but otoh certainly avoids corruption on either side (phone/server)
"""
def main(inf):
log = inf["log"]
vdir, fn = os.path.split(inf["vp"])
if not vdir.startswith("cam/sort"):
return {"rc": 0} # 2nd call after 1st reloc; already done
m = re.match(r"^[a-z]{3,4}_([0-9]{6})[0-9]{2}_[0-9]{6}.*\.(jpg|mp4)$", fn.lower())
if not m:
return {"rc": 0} # not recognized; write to original path in /cam/sort/
ym, ext = m.groups()
if ext == "jpg":
dst = f"/cam/pics/ts/{ym}/"
elif ext == "mp4":
dst = f"/cam/vids/ts/{ym}/"
else:
return {"rc": 0}
ap = "%s%s" % (inf["ap"].split("/cam/sort/")[0], dst)
if not os.path.isdir(ap):
os.makedirs(ap)
ret = {"rc": 0, "reloc": {"vp": dst}}
return ret
if __name__ == "__main__":
main()
+2 -2
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),
}
+3
View File
@@ -20,6 +20,8 @@ example:
example for running straight from source (instead of using an sfx):
PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd
NOT COMPATIBLE WITH: --th-bwrap
note that if you have python modules installed as --user (such as bpm/key detectors),
you should add /home/foo/.local as a VOLDIR
@@ -97,6 +99,7 @@ jail="${jail%/}"
# bind-mount system directories and volumes
mkdir -p "$jail"
for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq |
while IFS= read -r v; do
+55
View File
@@ -0,0 +1,55 @@
# thumbnail extractors
extract (or generate) thumbnails for custom file formats or override default thumbnail extraction for standard media files
## usage
* load plugins with `--th-extract baz=~/dev/copyparty/bin/thumbs/randomcolor.py`
* `baz` is the file extension (may be comma-separated list of extensions)
* add multiple different extractor plugins by repeating the `--th-extract` argument
## api
### in
each plugin must define a function `main(abspath, **kwargs)`:
* `abspath` is the path to a file to extract thumbnail for
* `kwargs` currently receives these keyword arguments:
* `vn` is the VFS which contains the requested file
* `th_srv` is an instance of [copyparty/th_srv](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/th_srv.py)
### out
the `main()` function must
* return `None` or raise an `Exception` in case it is unable to extract thumbnail
* return tuple `fmt, stream, offset, whence, size` otherwise:
* `fmt: str` – format (filename extension) of the thumbnail; should be one of the image formats supported by copyparty (see `--th-r-*` options)
* `stream: IO[bytes]` – binary file-like object supporting `seek()`, `read()`, `close()` methods; should contain extracted thumbnail image
* `offset: int, whence: int` – arguments to `stream.seek()`; copyparty will call `stream.seek(offset, whence)` once before starting reading the stream contents
* `size: int | None` – number of bytes to read from the `stream`; if `size` is negative or `None`, copyparty will read until the end of the stream
### notes
it is possible to extract or generate custom thumbnails for standard media files too,
e.g. run copyparty with `--th-extract mp3,m4a,aac,flac,opus=~/dev/copyparty/bin/thumbs/my_custom_mp3_th_extractor.py` and do your magic with audio file thumbnails
in case copyparty doesn't like the `fmt` option, it will `close()` the `stream` without ever calling `seek()` or `read()`
if your scenario involves heavy computation or i/o, it is better to defer the heavy parts until the first `seek()` or `read()` to avoid redundant work in case the result gets rejected based on `fmt` value
if you provide custom `stream` object:
copyparty performs buffered reading, so expect multiple `read()` calls, respect `size` argument in `read(size)` calls, and maintain correct seek position
## examples
* [randomcolor](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/randomcolor.py) generates a random .gif image
* [collabora](https://github.com/9001/copyparty/blob/hovudstraum/bin/thumbs/collabora.py) uses external self-hosted service (collabora online) to generate previews for various office documents, pdf and fb2 books, html, markdown, text files and some image formats; collabora is also supported as office editor in copyparty, see [wopi](https://github.com/9001/copyparty#wopi-server)
## some other known plugins seen on the internets
* [fpkg_thumb](https://github.com/kamaeff/fpkg-vault/blob/1c248de62a9ff9c4088d4b3e7962023cde9d6426/old/fpkg_thumb.py) extracts cover images from playstation4 software installation packages ("pkg" and "fpkg" files)
+125
View File
@@ -0,0 +1,125 @@
"""
CAUTION!
looks like collabora spawns separate jailed libreoffice instance to do the conversion
this may eat up too much ram in folders with many docs or with --th-pregen enabled
consider limiting resources available for collabora
e.g. for me these lines in docker-compose.yml helped alot:
deploy:
resources:
limits:
cpus: '2'
memory: '2G'
/CAUTION!
if you use collabora office editor with copyparty, you already have a thumbnailer for office formats:
https://sdk.collaboraonline.com/docs/conversion_api.html
the 'convert-to' api accepts files in multipart/form-data format
just add this extractor to copyparty with an argument like this:
--th-extract=docx,xlsx,pptx,odt,ods,pdf,fb2=~/dev/copyparty/bin/thumbs/collabora.py
and set COLLABORA_INSTANCE_URL below to your collabora instace url
full list of supported file types:
https://sdk.collaboraonline.com/docs/conversion_api.html#supported-input-formats
this is also an example of deferred IO:
no files opened and no network requests made if copyparty doesn't like the file type
"""
import os
import string
import random
import mimetypes
from urllib.error import HTTPError
from urllib.request import Request, urlopen
COLLABORA_INSTANCE_URL = "http://collabora:9980"
BOUNDARY_CHARS = string.ascii_letters + string.digits
def main(abspath, **kwargs):
boundary = "CopypartyFormData" + "".join(random.choices(BOUNDARY_CHARS, k=53))
start, end = make_headers(abspath, boundary)
size = len(start) + os.path.getsize(abspath) + len(end)
request = Request(
COLLABORA_INSTANCE_URL + "/cool/convert-to/png",
method="POST",
data=iterate(start, abspath, end),
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(size),
},
)
file_like = Wrapper(request)
offset, whence, length = 0, 0, None
return "png", file_like, offset, whence, length
def make_headers(abspath, boundary):
"""stuff to wrap file contents by for multipart"""
fname = "file." + abspath.rsplit(".", 1)[-1]
ftype = mimetypes.guess_file_type(fname)[0] or "application/octet-stream"
start = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="data"; filename="{fname}"\r\n'
f"Content-Type: {ftype}\r\n\r\n"
)
end = f"\r\n--{boundary}--"
return bytes(start, "ascii"), bytes(end, "ascii")
def iterate(start, fpath, end):
"""docs may be big so read and send them in chunks"""
yield start
with open(fpath, "rb") as f:
while True:
b = f.read(32768)
if not b:
break
yield b
yield end
class Wrapper:
"""
This file-like wrapper defers expensive work until first .seek() call
Useful to not waste resources in case copyparty decides
to throw our thumbnail away based on returned file extension
"""
def __init__(self, request):
self.request = request
self.response = None
def seek(self, offset, whence=0):
if self.response:
return 0
try:
# the heavy part:
# - reading probably big file
# - sending it over the network
# - making collabora do some work
self.response = urlopen(self.request, timeout=10)
except HTTPError as e:
e.close()
raise e
except Exception as e:
self.close()
raise e
# actual response is not seekable
# just return 0
return 0
def read(self, amount):
return self.response.read(amount)
def close(self):
if self.response:
self.response.close()
+29
View File
@@ -0,0 +1,29 @@
import io
import os
import struct
from base64 import b64decode
def create_gif() -> bytes:
head = r"R0lGODdhIAAgAIAB"
tail = r"AAD/LAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
#color = struct.pack(">L", random.randrange(0xffffff)) # wow, way too stable
color = b"\x00" + os.urandom(3)
return b64decode(head) + color + b64decode(tail)
def main(abspath, **ka) -> Optional[tuple[str, IO[bytes], int, int, Optional[int]]]:
print("hello from randomcolor.py pretending to extract a thumb from " + abspath)
# need to return a file-like object
gif_bytes = create_gif()
file_obj = io.BytesIO(gif_bytes)
# where copyparty should seek to in the "file" to start reading from
seek_offset = 0
seek_whence = 0
filesize = None # auto; read until end
# "gif" is the file-extension to decode the file as
return "gif", file_obj, seek_offset, seek_whence, filesize
+56 -18
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
S_VERSION = "2.21"
S_BUILD_DT = "2026-05-25"
S_VERSION = "2.25"
S_BUILD_DT = "2026-10-05"
"""
u2c.py: upload to copyparty
@@ -67,6 +67,8 @@ else:
WTF8 = "replace" if PY2 else "surrogateescape"
SEP = "{0}".format(os.sep).encode("ascii")
VT100 = platform.system() != "Windows"
@@ -249,7 +251,7 @@ class File(object):
def __init__(self, top, rel, size, lmod):
self.top = top # type: bytes
self.rel = rel.replace(b"\\", b"/") # type: bytes
self.rel = rel.replace(SEP, b"/") # type: bytes
self.size = size # type: int
self.lmod = lmod # type: float
@@ -267,6 +269,7 @@ class File(object):
self.wark = "" # type: str
self.url = "" # type: str
self.nhs = 0 # type: int
self.t_hs = 0.0 # type: float
# set by upload
self.t0_up = 0.0 # type: float
@@ -398,7 +401,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)
@@ -652,7 +655,7 @@ def walkdir(err, top, excl, seen):
def walkdirs(err, tops, excl):
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
sep = "{0}".format(os.sep).encode("ascii")
sep = SEP
if not VT100:
excl = excl.replace("/", r"\\")
za = []
@@ -725,7 +728,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
@@ -733,16 +736,22 @@ def up2k_chunksize(filesize):
stepsize *= mul
# mostly from copyparty/up2k.py
def get_hashlist(file, pcb, mth):
# type: (File, Any, Any) -> None
with open(file.abs, "rb", 512 * 1024) as f:
get_hashlist_f(file, pcb, mth, f)
# mostly from copyparty/up2k.py
def get_hashlist_f(file, pcb, mth, f):
# type: (File, Any, Any, Any) -> None
"""generates the up2k hashlist from file contents, inserts it into `file`"""
chunk_sz = up2k_chunksize(file.size)
file_rem = file.size
file_ofs = 0
ret = []
with open(file.abs, "rb", 512 * 1024) as f:
if True:
t0 = time.time()
if mth and file.size >= 1024 * 512:
@@ -778,6 +787,18 @@ def get_hashlist(file, pcb, mth):
file.kchunks[k] = [v1, v2]
def wark_stdin(ar):
file = File(b"", b"", int(ar.files[0]), 0)
get_hashlist_f(file, None, False, sys.stdin if PY2 else sys.stdin.buffer)
if ar.chs:
zsl = ["%s %d %d" % (zsii[0], n, zsii[1]) for n, zsii in enumerate(file.cids)]
print("chs:\n" + "\n".join(zsl))
zsl = [ar.wsalt, str(file.size)] + [x[0] for x in file.cids]
zb = hashlib.sha512("\n".join(zsl).encode("utf-8")).digest()[:33]
wark = ub64enc(zb).decode("utf-8")
print(wark)
def printlink(ar, purl, name, fk):
if not name:
url = purl # srch
@@ -886,6 +907,7 @@ def handshake(ar, file, search):
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
file.name = r["name"]
file.wark = r["wark"]
file.t_hs = time.time()
if ar.uon and not r["hash"]:
printlink(ar, file.url, r["name"], r.get("fk"))
@@ -1009,7 +1031,7 @@ class Ctl(object):
self.up_b = 0 # num bytes handled
self.up_br = 0 # num bytes actually transferred
self.uploader_busy = 0
self.serialized = False
self.oneconn = False
self.t0 = time.time()
self.t0_up = None
@@ -1195,14 +1217,15 @@ class Ctl(object):
def hasher(self):
ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None
sep = "{0}".format(os.sep).encode("ascii")
usep = os.sep
sep = SEP
prd = None
ls = {}
for top, rel, inf in self.filegen:
isdir = stat.S_ISDIR(inf.st_mode)
if self.ar.z or self.ar.drd:
rd = rel if isdir else os.path.dirname(rel)
srd = rd.decode("utf-8", "replace").replace("\\", "/").rstrip("/")
srd = rd.decode("utf-8", "replace").replace(usep, "/").rstrip("/")
if srd:
srd += "/"
if prd != rd:
@@ -1212,7 +1235,7 @@ class Ctl(object):
print(" ls ~{0}".format(srd))
zt = (
self.ar.vtop,
quotep(rd.replace(b"\\", b"/")).decode("utf-8"),
quotep(rd.replace(sep, b"/")).decode("utf-8"),
)
sc, txt = web.req("GET", "%s%s?ls&lt&dots" % zt, {})
if sc >= 400:
@@ -1385,10 +1408,10 @@ class Ctl(object):
self.recheck.append(file)
with self.mutex:
if hs and not sprs and not self.serialized:
t = "server filesystem does not support sparse files; serializing uploads\n"
if hs and not sprs and not self.oneconn:
t = "server filesystem does not support sparse files; cannot parallelize uploads\n"
eprint(t)
self.serialized = True
self.oneconn = True
for _ in range(self.ar.j - 1):
self.q_upload.put(None)
if not hs:
@@ -1400,7 +1423,7 @@ class Ctl(object):
if not file.recheck:
self.up_done(file)
if hs and file.up_c:
if hs and file.up_c and not self.oneconn:
# some chunks failed
self.up_c -= len(hs)
file.up_c -= len(hs)
@@ -1458,6 +1481,7 @@ class Ctl(object):
self.q_upload.put(fsl)
def uploader(self):
drop_hs = (0.0, "")
while True:
fsl = self.q_upload.get()
if not fsl:
@@ -1476,6 +1500,9 @@ class Ctl(object):
cids = fsl.cids
self.last_file = file
if self.oneconn and drop_hs == (file.t_hs, file.wark):
continue
with self.mutex:
if not self.uploader_busy:
self.at_upr = time.time()
@@ -1501,6 +1528,9 @@ class Ctl(object):
t = "upload failed, retrying: %s #%s+%d (%s)\n"
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
file.cd = time.time() + self.ar.cd
if self.oneconn:
file.ucids = []
drop_hs = (file.t_hs, file.wark)
# handshake will fix it
with self.mutex:
@@ -1558,7 +1588,7 @@ NOTE: if server has --usernames enabled, then password is "username:password"
ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=files_decoder, nargs="+", help="files and/or folders to process")
ap.add_argument("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW')")
ap.add_argument("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW'), default is env.var U2C_PW")
ap.add_argument("--ba", metavar="PASSWD", default="", help="password (or $filepath) for basic-auth (usually not necessary)")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
@@ -1587,6 +1617,7 @@ NOTE: if server has --usernames enabled, then password is "username:password"
ap.add_argument("--wsalt", type=unicode, metavar="S", default="hunter2", help="salt to use when creating warks; must match server config")
ap.add_argument("--chs", action="store_true", help="verbose (print the hash/offset of each chunk in each file)")
ap.add_argument("--jw", action="store_true", help="just identifier+filepath, not mtime/size too")
ap.add_argument("--stdin", action="store_true", help="calculate file-ID of stdin; u2c.py --stdin - $LEN < a.mkv")
ap = app.add_argument_group("performance tweaks")
ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections")
@@ -1617,6 +1648,10 @@ NOTE: if server has --usernames enabled, then password is "username:password"
except:
pass
if ar.stdin:
wark_stdin(ar)
return
# msys2 doesn't uncygpath absolute paths with whitespace
if not VT100:
zsl = []
@@ -1706,6 +1741,9 @@ NOTE: if server has --usernames enabled, then password is "username:password"
with open(zs[1:], "rb") as f:
setattr(ar, k, f.read().decode("utf-8").strip())
if not ar.a:
ar.a = os.environ.get("U2C_PW", "")
if ar.ba:
ar.ba = "Basic " + base64.b64encode(ar.ba.encode("utf-8")).decode("utf-8")
+4
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
+1
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;
};
+11 -13
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.14"
pkgver="1.20.25"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -13,23 +13,22 @@ depends=("bash" "python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"cfssl: generate TLS certificates on startup"
"python-mutagen: music tags (alternative)"
"python-paramiko: sftp server",
"python-mutagen: music tags (fast/inaccurate)"
"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)"
"python-paramiko: sftp server"
"python-pyftpdlib: ftp server"
"python-pyopenssl: ftps server (needs pyftpdlib too)"
"libkeyfinder: detection of musical keys"
"python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks"
"python-argon2-cffi: hashed passwords in config"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12")
backup=("etc/copyparty.conf")
install=copyparty.install
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
}
@@ -38,12 +37,11 @@ package() {
cd "${srcdir}/${pkgname}-${pkgver}"
python -m installer --destdir="$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/copyparty.conf"
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/srv/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}
+27
View File
@@ -0,0 +1,27 @@
post_upgrade() {
# v1.20.22 moved the config-file, affecting the system-service only
if [[ $(vercmp "$2" 1.20.22) -lt 0 ]]; then
oldpath="/etc/copyparty/copyparty.conf"
newpath="/etc/copyparty.conf"
echo "==> Migrating config-file from $oldpath to $newpath"
if [[ ! -f "$oldpath" ]]; then
oldpath="$oldpath.pacsave"
fi
if [[ -f "$newpath.pacnew" ]]; then
echo "==> WARNING: A config-file already existed in the new location [$newpath]. The existing file will be used as-is."
elif [[ -f "$oldpath" ]]; then
mv -v "$oldpath" "$newpath"
rmdir /etc/copyparty 2>/dev/null || true
fi
systemctl cat copyparty@.service | awk '/=PRTY_CONFIG=/ && !/=\/etc\/copyparty.conf/ {exit 1}' || {
echo "==> WARNING: The copyparty system-service has been edited by hand since installation, so it will not read config from the new default config path [$newpath]. Please use PRTY_CONFIG=$newpath if that is correct for your setup."
}
fi
}
+2 -5
View File
@@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.14
pkgver=1.20.25
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -21,12 +21,9 @@ 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=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12")
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
}
@@ -15,7 +15,7 @@ Description=copyparty file server
Type=notify
SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+12 -2
View File
@@ -1,3 +1,13 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
please add some `*.conf` files to `/etc/copyparty.d/`
please edit the config-file and restart copyparty;
* if running as a system service: `vim /etc/copyparty.conf`
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
some inspiration:
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
the full list of configuration options can be seen at https://copyparty.eu/cli/
or by running `copyparty --help`
@@ -17,7 +17,7 @@ Description=copyparty file server
[Service]
SyslogIdentifier=prisonparty
Environment=PYTHONUNBUFFERED=x
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
ExecReload=/bin/kill -s USR1 $MAINPID
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
+1 -1
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,
+3 -3
View File
@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.20.14/copyparty-1.20.14.tar.gz",
"version": "1.20.14",
"hash": "sha256-h4Pcg5C+F2c9MG9CTnoo3Z+bT84AXjVzTTDBspZwfBI="
"url": "https://github.com/9001/copyparty/releases/download/v1.20.25/copyparty-1.20.25.tar.gz",
"version": "1.20.25",
"hash": "sha256-gsaN+kLgill1e7+gsdFAAyFBF1NHkG4Fa++DkE9cLHA="
}
+1 -1
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; };
-3
View File
@@ -26,9 +26,6 @@ See release at https://github.com/9001/copyparty/releases
%setup -q
%build
cd "copyparty/web"
make
cd -
%pyproject_wheel
%install
+4 -1
View File
@@ -66,11 +66,14 @@ var u2min = `
</style>
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
<a href="#" id="u2min_off">show advanced options</a>
`;
if (!has(perms, 'read')) {
var e2 = mknod('div');
e2.innerHTML = u2min;
ebi('wrap').insertBefore(e2, QS('#wfp'));
ebi('u2min_off').onclick = function () {
this.parentNode.innerHTML='';
};
}
+3 -3
View File
@@ -1,5 +1,5 @@
# this will start `/usr/bin/copyparty`
# and read config from `$HOME/.config/copyparty.conf`
# and read config from `$HOME/.config/copyparty/copyparty.conf`
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
@@ -11,13 +11,13 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# ensure there is a config
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi'
ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp -v /etc/copyparty.conf %h/.config/copyparty/; fi'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
+13 -11
View File
@@ -1,13 +1,15 @@
[global]
i: 127.0.0.1
[accounts]
user: password
[/]
/var/lib/copyparty-jail
/var/lib/copyparty-jail/srv/
accs:
r: *
rwdma: user
flags:
grid
r: * # everyone can [r]ead (browse/download)
# above is a very simple config;
# the webroot is /var/lib/copyparty-jail/srv/
# readable by everyone, no login required
#
# you'll want to replace this file; some inspiration:
# - a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
# - another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
# - and CTRL-F "config file example" here: https://github.com/9001/copyparty
# - full list of options: https://copyparty.eu/cli/
-3
View File
@@ -3,9 +3,6 @@
# vim: ft=yaml:
# put this file in /etc/
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
+3 -3
View File
@@ -1,5 +1,5 @@
# this will start `/usr/bin/copyparty`
# and read config from `/etc/copyparty/copyparty.conf`
# and read config from `/etc/copyparty.conf`
#
# the %i refers to whatever you put after the copyparty@
# so with copyparty@foo.service, %i == foo
@@ -14,9 +14,9 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
Environment=PRTY_CONFIG=/etc/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
+9 -6
View File
@@ -1,10 +1,13 @@
this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured
this is `/var/lib/copyparty-jail/srv/`, the fallback webroot when copyparty has not yet been configured
please edit `/etc/copyparty/copyparty.conf` (if running as a system service)
or `$HOME/.config/copyparty/copyparty.conf` if running as a user service
please edit the config-file and restart copyparty;
* if running as a system service: `vim /etc/copyparty.conf`
* if running as a user service: `vim $HOME/.config/copyparty/copyparty.conf`
a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf
some inspiration:
* a basic example config: https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf
* another example with focus on syntax: https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf
* and CTRL-F `config file example` here: https://github.com/9001/copyparty
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
the full list of configuration options can be seen at https://copyparty.eu/cli/
or by running `copyparty --help`
+3 -3
View File
@@ -1,6 +1,6 @@
# this will start `/usr/bin/copyparty`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty/copyparty.conf`
# and read copyparty config from `/etc/copyparty.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `%i` and `--`
@@ -19,9 +19,9 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=prisonparty
WorkingDirectory=/var/lib/copyparty-jail
WorkingDirectory=/var/lib/copyparty-jail/srv
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
Environment=PRTY_CONFIG=/etc/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
@@ -0,0 +1,36 @@
# docker-compose.yml for copyparty
services:
copyparty:
image: copyparty/ac:latest
container_name: copyparty
user: "1000:1000"
volumes:
- ./:/cfg:z
- /path/to/your/fileshare/top/folder:/w:z
environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
healthcheck:
# hide it from logs with "/._" so it matches the default --lf-url filter
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset=/._"]
interval: 1m
timeout: 2s
retries: 5
start_period: 15s
networks:
- traefik_default
labels:
- traefik.http.routers.copyparty.rule=Host(`<DOMAIN>`)
- traefik.http.services.copyparty.loadbalancer.server.port=3923
- traefik.http.routers.copyparty.tls.certresolver=le
- traefik.http.routers.copyparty.entrypoints=websecure
networks:
traefik_default:
external: true
@@ -0,0 +1,43 @@
# note: copied from copyparty/docs/examples/docker/basic-docker-compose/copyparty.conf
# not actually YAML but lets pretend:
# -*- mode: yaml -*-
# vim: ft=yaml:
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # enable multimedia indexing
ansi # enable colors in log messages (both in logfiles and stdout)
# q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker
# p: 3939 # listen on another port
# ipa: 10.89. # only allow connections from 10.89.*
# ipa: 172.16.4.0/23 # ...or only 172.16.4.* and 172.16.5.*
# ipa: lan # ...or allow LAN only; reject internet IPs
# df: 16 # stop accepting uploads if less than 16 GB free disk space
# ver # show copyparty version in the controlpanel
# grid # show thumbnails/grid-view by default
# theme: 2 # monokai
# name: datasaver # change the server-name that's displayed in the browser
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
# no-robots, force-js # make it harder for search engines to read your server
# enable version-checking by uncommenting one of the vc-url lines below;
# shows a warning-banner in the controlpanel if your version has a known vulnerability
#vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9
#vc-url: https://api.copyparty.eu/advisories
vc-exit # panic and shutdown instead of just showing the warning
[accounts]
ed: wark # username: password
[/] # create a volume at "/" (the webroot), which will
/w # share /w (the docker data volume)
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin
flags:
e2ds # enable filesystem-scanning for this volume only
@@ -0,0 +1,34 @@
# docker-compose.yml for traefik
services:
traefik:
image: traefik:latest
command:
- --api.insecure=false
- --api.dashboard=true
- --providers.docker=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entryPoints.websecure.http.tls=true
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=<your email here>
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./letsencrypt:/letsencrypt
labels:
# OPTIONAL: host your traefik dashboard on <DOMAIN>
# - traefik.http.routers.traefik-dash.rule=Host(`<DOMAIN>`)
# - traefik.http.routers.traefik-dash.entrypoints=websecure
# - traefik.http.routers.traefik-dash.service=api@internal
# - traefik.http.routers.traefik-dash.tls.certresolver=le
# - traefik.http.routers.traefik-dash.middlewares=traefik-dash-auth,traefik-dash-redirect
# - traefik.http.middlewares.traefik-dash-auth.basicauth.users=<USERNAME>:<HTPASSWD_ENCODED_PASSWD>
# - traefik.http.middlewares.traefik-dash-redirect.redirectregex.regex=^https://<SUBDOMAIN>\.<DOMAIN>\.TLD/?$
# - traefik.http.middlewares.traefik-dash-redirect.redirectregex.replacement=https://<DOMAIN>/dashboard/
networks: {}
+6 -2
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
@@ -40,6 +40,9 @@ VT100 = "--ansi" in sys.argv or (
)
# introduced in anniversary update
zs = os.environ.get("PRTY_CONFIG", "")
CFG_DEF = [zs] if zs else []
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]
MACOS = platform.system() == "Darwin"
@@ -80,6 +83,7 @@ web/deps/marked.js
web/deps/mfusepy.py
web/deps/mini-fa.css
web/deps/mini-fa.woff
web/deps/orbitron.woff2
web/deps/prism.css
web/deps/prism.js
web/deps/prismd.css
@@ -134,7 +138,7 @@ web/tl/vie.js
web/ui.css
web/up2k.js
web/util.js
web/w.hash.js
web/wopi.html
"""
RES = set(zs.strip().split("\n"))
RESM = {
+320 -48
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>"
@@ -14,6 +14,7 @@ import locale
import os
import re
import select
import shutil
import socket
import sys
import threading
@@ -23,11 +24,13 @@ import uuid
from .__init__ import (
ANYWIN,
CFG_DEF,
CORES,
EXE,
MACOS,
PY2,
PY36,
UNIX,
VT100,
WINDOWS,
E,
@@ -37,7 +40,10 @@ from .__init__ import (
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
from .bos import bos
from .cert import NO_TLS
from .cfg import flagcats, onedash
from .mdns import DNS_VND
from .qrkode import VENDORED as QR_VND
from .svchub import SvcHub
from .util import (
APPLESAN_TXT,
@@ -45,7 +51,10 @@ from .util import (
DEF_EXP,
DEF_MTE,
DEF_MTH,
HAVE_BWRAP,
HAVE_IPV6,
HAVE_MAGIC,
IFADDR_VND,
IMPLICATIONS,
JINJA_VER,
MIKO_VER,
@@ -61,6 +70,7 @@ from .util import (
URL_BUG,
URL_PRJ,
Daemon,
absreal,
align_tab,
b64enc,
ctypes,
@@ -92,20 +102,19 @@ if PY2:
range = xrange # type: ignore
try:
if os.environ.get("PRTY_NO_TLS"):
if NO_TLS:
raise Exception()
HAVE_SSL = True
import ssl
HAVE_SSL = True
except:
HAVE_SSL = False
u = unicode
zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
CFG_DEF.pop()
FULL_HELP = os.environ.get("PRTY_FULL_HELP", "")
class RiceFormatter(argparse.HelpFormatter):
@@ -582,6 +591,17 @@ def sfx_tpoke(top: str):
lprint("<TPOKE> [%s] %r" % (f, ex))
files.remove(f)
r = os.path.dirname(top)
for d in os.listdir(r):
try:
p = r + "/" + d
f = p + "/copyparty/up2k.py"
t2 = os.path.getmtime(f)
if t - t2 > 777777 and d.startswith("pe-copyparty"):
shutil.rmtree(p)
except:
pass
time.sleep(78123)
@@ -592,12 +612,64 @@ def showlic() -> None:
except:
buf = b""
if buf:
print(buf.decode("utf-8", "replace"))
else:
if not buf:
print("no relevant license info to display")
return
lic = buf.decode("utf-8", "replace")
try:
import site
from importlib import import_module
from inspect import getsourcefile
bases = site.PREFIXES + [site.USER_BASE]
bases = [x.replace(os.sep, "/") + "/" for x in list(set(bases)) if x]
except:
bases = []
# remove missing and/or non-vendored
for url, zi, zx in (
("/dnslib/", 1, DNS_VND),
("/ifaddr/", 1, IFADDR_VND),
("/nayuki/QR", 1, QR_VND),
("/partftpy", 2, PARTFTPY_VER),
("/jinja/", 3, "jinja2"),
("/markupsafe/", 3, "jinja2"), # jinja2 sic
("/pyftpdlib/", 2, PYFTPD_VER), # heed PRTY_NO
("/pyftpdlib/", 3, "pyftpdlib"),
("/python-magic/", 1, HAVE_MAGIC), # heed PRTY_NO
("/python-magic/", 3, "magic"),
):
if url not in lic:
continue
elif zi == 1:
if zx:
continue
elif zi == 2:
if zx != "(None)":
continue
elif zi == 3:
mod = None
try:
assert bases and getsourcefile and import_module # type: ignore # !rm
mod = import_module(zx) # type: ignore
mp = getsourcefile(mod) or ""
for zs in bases:
if mp.startswith(zs):
mp = ""
if mp:
continue
except:
if mod:
continue
zs1, zs2 = lic.split(url, 1)
zs1 = zs1.rsplit("\n", 1)[0]
zs2 = zs2.split("\n\n", 1)[1]
lic = zs1 + "\n" + zs2
print(lic)
def get_sects():
return [
@@ -615,6 +687,7 @@ def get_sects():
\033[32m-i 127.0.0.1\033[0m listens on IPv4 localhost only
\033[32m-i 127.1\033[0m listens on IPv4 localhost only
\033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1
\033[32m-i no\033[0m disable webserver
\033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on;
the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m
@@ -775,6 +848,122 @@ def get_sects():
).rstrip()
+ build_flags_desc(),
],
[
"dedup",
"file deduplication on upload",
dedent(
"""
when just \033[36m--dedup\033[0m or volflag "\033[36mdedup\033[0m" is enabled, files will be
deduplicated by means of \033[1msymlinks,\033[0m which demand some caution:
the available dedup approaches:
* symlinks work on most filesystems, but if another software
moves either the symlink itself or the file that it points to,
the link will break and the file "disappears";
only safe if only copyparty is used to rename/move/delete
* hardlinks work on Linux filesystems and \033[1mlook\033[0m like regular files,
except if the contents of one copy of that file is edited, then
this will affect \033[1mall the dupes too!\033[0m
and they share the same last-modified-time
* reflinks only work on btrfs/zfs/xfs but is the safest/best choice;
deduplicated files behave just like regular files,
and can be modified without affecting other copies/dupes
to change approach, add one of the following in addition to "\033[36mdedup\033[0m":
\033[36m--reflink\033[0m or volflag "\033[36mreflink\033[0m"
if reflink is not possible (unsupp/cross-fs), a full copy is made
\033[36m--hardlink-only\033[0m or volflag "\033[36mhardlinkonly\033[0m"
if hardlink is not possible (unsupp/cross-fs), a full copy is made
\033[36m--hardlink\033[0m or volflag "\033[36mhardlink\033[0m"
if hardlink is not possible (unsupp/cross-fs), a symlink is made
note: dedup works best when combined with e2ds or e2dsa
to deduplicate existing files on-disk, see \033[36m--help-redup\033[0m
"""
),
],
[
"redup",
"deduplicating existing files on disk",
dedent(
"""
\033[36m--redup\033[0m can convert on-disk files between deduplication-approaches,
for example turning redundant full copies into reflinks to save space
converts \033[1mall files\033[0m in each volume! not just copyparty-created ones
example: --redup \033[32mno,sym,hard\033[0m=\033[35mref\033[0m will find all files of type
\033[32mfullcopy/symlink/hardlink\033[0m, and convert them into \033[35mreflinks\033[0m
(see \033[36m--help-dedup\033[0m for a summary on the types)
converting \033[1mfrom\033[0m symlinks/hardlinks is fairly fast and simple;
in this case, the obvious target type/approach is reflinks
(which only works on btrfs/zfs/xfs)
converting \033[1mto\033[0m symlinks or hardlinks is \033[3mpossible\033[0m, but
not-recommended due to risks explained in \033[36m--help-dedup\033[0m
(interaction with other softwares can result in data loss)
converting \033[1mfrom\033[0m 'no' (full redundant copies) is hella slow;
checksums each file to avoid corruption from bad RAM
"""
),
],
[
"thumb-ex",
"extract thumbnails from custom file formats with python scripts",
dedent(
"""
copyparty is able to extract thumbnails from various media formats:
images, audio, video, ebooks
if you want to extract thumbnails from unsupported file formats,
you can load a plugin, which extracts image data from custom formats
load the plugin using --args; for example \033[36m
--tx-extract iso,mdf,dmg=~/party-thumb-extractors/th_diskimg.py
\033[0m
the file must define the function \033[35mmain(abspath, **kwargs)\033[0m:
\033[35mabspath\033[0m: path to the file to extract thumbnail from
\033[35mkwargs\033[0m currently have:
\033[35mvn\033[0m: the VFS which contains the requested file
\033[35mrth_srv\033[0m: the copyparty ThumbSrv instance
if `main` couldn't extract a thumbnail, it must return None;
otherwise the return value must be a tuple:
\033[35mreturn fmt, stream, offset, whence, size\033[0m
> \033[32mfmt\033[0m: thumbnail format, should be one of the formats
supported by copyparty; e.g. "png", "jpg"
> \033[32mstream\033[0m: binary file-like object containing the thumbnail,
supporting seek(), read(), and close() methods
> \033[32moffset, whence\033[0m: arguments to seek() method;
stream.seek(offset, whence) will be called once before copyparty
starts reading from the stream
> \033[32msize\033[0m: number of bytes to read ftom the stream;
if negative, copyparty will read through the end of the stream;
you can also supply extractors for standard formats to override default
thumbnail extraction behaviour; for example \033[36m
--tx-extract mp3=~/party-thumb-extractors/th_mp3.py
\033[0mwill let you extract or generate custom thumbnails for mp3 files
\033[1;35mPS!\033[0m the folder that contains the python file should ideally
not contain many other python files, and especially nothing
with filenames that overlap with modules used by copyparty
"""
),
],
[
"handlers",
"use plugins to handle certain events",
@@ -1038,7 +1227,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
(blank) = 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
"""
),
],
@@ -1087,9 +1281,9 @@ def get_sects():
"file/folder permissions",
dedent(
"""
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
global-option \033[33m--chmod-f\033[0m and volflag "\033[33mchmod_f\033[0m" specifies the unix-permission to use when creating a new file
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
similarly, \033[33m--chmod-d\033[0m and "\033[33mchmod_d\033[0m" sets the directory/folder perm
the value is a three- or four-digit octal number such as \033[32m755\033[0m, \033[32m0644\033[0m, \033[32m2750\033[0m, etc.
@@ -1256,8 +1450,8 @@ def add_qr(ap, tty):
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without \033[33m--qr-pin\033[0m in case of issues)")
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when \033[33m--qr-pin\033[0m is enabled, check for terminal size change every \033[33mSEC\033[0m")
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
@@ -1302,16 +1496,18 @@ def add_upload(ap):
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see \033[33m--help-chmod\033[0m. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see \033[33m--help-chmod\033[0m. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication; see \033[33m--help-dedup\033[0m (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
ap2.add_argument("--redup", metavar="A[,B]=T", help="convert dedup-type for existing files; \033[33mA,B\033[0m is dedup-types to convert from (no/ref/sym/hard), \033[33mT\033[0m is target type; converting from sym/hard is fast-ish, from no/ref is slooow, to sym/hard is jank; see \033[33m--help-redup\033[0m. Example: [\033[32msym,hard=ref\033[0m] (volflag=redup)")
ap2.add_argument("--redup-dry", action="store_true", help="dry-run; makes \033[33m--redup\033[0m not apply changes (volflag=redup_dry)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)")
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
@@ -1325,11 +1521,11 @@ 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")
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
ap2.add_argument("--u2sz", metavar="N,N", type=u, default="64,96", help="web-client: default upload chunksize in MiB, either \033[33mtarget,max\033[0m or just \033[33mtarget\033[0m (sets both); this is \033[33mtarget\033[0m in the web-UI. Each HTTP POST will aim for \033[33mtarget\033[0m and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Minimum is [\033[32m1\033[0m] and will be slow. Lowest recommended is [\033[32m8\033[0m]. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32mno\033[0m]")
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
@@ -1352,9 +1548,9 @@ def add_network(ap):
ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)")
ap2.add_argument("--http-vary", metavar="TXT", type=u, default="Origin, PW, Cookie", help="value of the 'Vary' response-header; a hint for caching proxies")
ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets")
if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
elif not MACOS:
if FULL_HELP or ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets (windows-only); allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
if FULL_HELP or (not ANYWIN and not MACOS):
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
@@ -1405,7 +1601,7 @@ def add_auth(ap):
ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see \033[33m--help-idp\033[0m")
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
@@ -1417,13 +1613,13 @@ def add_auth(ap):
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see \033[33m--help-auth-ord\033[0m")
ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
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]")
@@ -1503,8 +1699,12 @@ def add_sftp(ap):
ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)")
ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username")
ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated")
ap2.add_argument("--sftp-fastldr", action="store_true", help="speedhack for 'loading hostkeys' on old CPUs; only use with known-good hostkeys")
ap2.add_argument("--sftp-chattr", metavar="T", default="s", help="which attributes can be modified with chattr? [\033[32ms\033[0m]=size, [\033[32mt\033[0m]=mtime, [\033[32mp\033[0m]=permissions, [\033[32mu\033[0m]=uid/gid. Example: [\033[32mstpu\033[0m]")
ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath")
ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--sftp-hs-t", metavar="SEC", type=int, default=15, help="connection handshake timeout in seconds")
ap2.add_argument("--sftp-hs-n", metavar="NUM", type=int, default=4, help="max num ongoing/incomplete handshakes")
def add_ftp(ap):
@@ -1519,6 +1719,7 @@ def add_ftp(ap):
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections")
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, default="", help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
ap2.add_argument("--ftp-banner", metavar="T", type=u, default="welcome to the party", help="bannertext to send when someone connects; \\n is newline, can be @filepath (loaded into memory on startup); if it has a newline then banner must be longer than 75 chars")
def add_webdav(ap):
@@ -1570,6 +1771,19 @@ 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-accs", metavar="TXT", type=u, default="", help="restrict wopi to this comma-separated list of usernames; default is everyone")
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="H=U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m maps http \033[33mH\033[0mOST copyparty being accessed by to specific WOPI client instance \033[33mU\033[0mRL; 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")
@@ -1606,8 +1820,10 @@ def add_stats(ap):
def add_yolo(ap):
ap2 = ap.add_argument_group("yolo options")
ap2.add_argument("--hack-me-bro", action="store_true", help="allow enabling dangerously buggy protocols such as \033[33m--smb\033[0m / \033[33m--tftp\033[0m")
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \033[1;31mDANGEROUS\033[0m / LAN-only")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--allow-svg-js", action="store_true", help="allow svg images to execute javascript; default-disabled because ~nobody wants it (volflag=allow_svg_js)")
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET -- \033[1;31mDANGEROUS\033[0m, removes csrf protection")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
@@ -1643,6 +1859,29 @@ def add_optouts(ap):
def add_safety(ap):
th_bwrap = ""
if HAVE_BWRAP:
zsl = [
HAVE_BWRAP,
"--proc /proc",
"--tmpfs /tmp",
"--tmpfs /var",
"--tmpfs /run",
"--dev-bind /dev/null /dev/null",
"--dev-bind /dev/random /dev/random",
"--dev-bind /dev/urandom /dev/urandom",
"--chdir /tmp",
"--clearenv",
"--unshare-all",
"--cap-drop ALL",
"--die-with-parent",
"--new-session",
]
for d in ("/lib", "/lib64", "/usr/lib", "/usr/lib64"):
if os.path.isdir(d):
zsl.append(" --ro-bind %s %s" % (d, d))
th_bwrap = " ".join(zsl)
ap2 = ap.add_argument_group("safety options")
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --no-html --no-readme --no-logues --unpost=0 --no-del --no-mv --reflink --dav-auth --vague-403 -nih")
@@ -1654,10 +1893,13 @@ def add_safety(ap):
ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)")
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings")
ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites (volflag=noscript)")
ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables --no-script (volflag=nohtml)")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings (volflag=no_logues)")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings (volflag=no_readme)")
ap2.add_argument("--csp-ui", metavar="TXT", default="script-src 'unsafe-eval' 'nonce-{{ js_nonce }}'; worker-src 'self'", help="content-security-policy to apply for the web-UI; default helps prevent XSS by blocking <script> / onclick / ... (volflag=csp_ui)")
ap2.add_argument("--csp-dl", metavar="TXT", default="", help="content-security-policy to apply for static files (volflag=csp_dl)")
ap2.add_argument("--no-script", action="store_true", help="disables javascript in html files; helps prevent XSS but kills interactive websites; this will override \033[33m--csp-dl\033[0m with [\033[32mscript-src 'none'\033[0m] (volflag=noscript)")
ap2.add_argument("--no-html", action="store_true", help="show html-files as plain text; helps prevent XSS but kills websites/blogs, also enables \033[33m--no-script\033[0m (volflag=nohtml)")
ap2.add_argument("--no-mime", action="store_true", help="disallow changing the response mimetype with url-parameter ?mime=... (volflag=nomime)")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
@@ -1678,10 +1920,18 @@ def add_safety(ap):
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
ap2.add_argument("--acao-re", metavar="PTN", type=u, default="", help="Access-Control-Allow-Origin; supplement to \033[33m--acao\033[0m; allow request if http-header 'Origin' matches either \033[33m--acao\033[0m or regex-\033[33mPTN\033[0m; ominous example: [\033[32m^moz-extension://.*\033[0m]")
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)")
else:
ap2.add_argument("--use-bwrap", metavar="TXT", type=u, default="n", help=argparse.SUPPRESS)
def add_salt(ap, fk_salt, dk_salt, ah_salt):
def add_salt(ap, fk_salt, dk_salt, ah_salt, wopi_salt):
ap2 = ap.add_argument_group("salting options")
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
@@ -1690,6 +1940,7 @@ def add_salt(ap, fk_salt, dk_salt, ah_salt):
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
ap2.add_argument("--wopi-salt", metavar="TXT", type=u, default=wopi_salt, help="WOPI file ID salt; used by \033[33m--wopi\033[0m for persistent file IDs")
ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
@@ -1707,7 +1958,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")
@@ -1758,33 +2009,44 @@ 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")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)")
ap2.add_argument("--th-vips-jxl", metavar="N", type=int, default=1, help="when to allow generating jxl thumbnails with libvips; 0=never, 1=musl-mallocng, 2=always")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds; 0=no-keepalive")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \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-pil-add", metavar="T", type=u, default="", help="add or remove image formats to decode using pillow listed in \033[33m--th-r-pil\033[0m; example: [\033[32mfb2,pdf,-psd,-bmp\033[0m] (add fb2 and pdf, remove psd and bmp)")
ap2.add_argument("--th-vips-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for pyvips")
ap2.add_argument("--th-raw-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for rawpy/libraw")
ap2.add_argument("--th-ffi-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg images")
ap2.add_argument("--th-ffv-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg videos")
ap2.add_argument("--th-ffa-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg audio")
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")
ap2.add_argument("--th-extract", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m list of file extensions to thumbnail using a custom plugin (a python script); example: [\033[32mmdf,iso,dmg=/thumbs/diskimg.py\033[0m], see \033[33m--help-thumb-ex\033[0m")
ap2.add_argument("--th-extr-sz", metavar="M", type=int, default=16, help="max num megabytes to allow \033[33m--th-extract\033[0m plugins to extract from each file")
ap2.add_argument("--hot-th-extr", action="store_true", help="recompile extractors on each thumbnail extraction -- expensive but convenient when hacking on stuff")
def add_transcoding(ap):
@@ -1841,11 +2103,13 @@ def add_db_general(ap, hcores):
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
ap2.add_argument("--hist-cow", action="store_true", help="btrfs-only: histpaths (db/thumbs) are nocow by default; if you really need Copy-on-write then enable this (volflag=hist_cow)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="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)")
@@ -1875,7 +2139,9 @@ def add_txt(ap):
ap2 = ap.add_argument_group("textfile options")
ap2.add_argument("--rw-edit", metavar="T,T", type=u, default="md", help="comma-sep. list of file-extensions to allow editing with permissions read+write; all others require read+write+delete (volflag=rw_edit)")
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of textfiles; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--md-nhist", metavar="TXT", type=int, default=99, help="how many old versions of textfiles to keep; 0=infinite (volflag=md_nhist)")
ap2.add_argument("--show-hist", action="store_true", help="allow browsing the .hist folder; enable to view old markdown files (volflag=show_hist)")
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
@@ -1934,8 +2200,8 @@ def add_ui(ap, retry: int):
ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html")
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template; \033[32m<script>\033[0m will not work, use \033[32m<script nonce=\"{{ js_nonce }}\">\033[0m (volflag=html_head)")
ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) \033[33m--html-head\033[0m but only accepts static text (volflag=html_head_s)")
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
@@ -1946,7 +2212,7 @@ def add_ui(ap, retry: int):
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as \033[33m--ver-who all\033[0m")
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
@@ -1977,16 +2243,16 @@ def add_debug(ap):
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies")
if hasattr(select, "poll"):
if FULL_HELP or hasattr(select, "poll"):
ap2.add_argument("--no-poll", action="store_true", help="kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)")
ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead")
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
if ANYWIN or sys.version_info < (3, 7):
ap2.add_argument("--sig-thr", action="store_true", default=True, help=argparse.SUPPRESS)
else:
if sys.version_info > (3, 7) and not ANYWIN:
ap2.add_argument("--sig-thr", action="store_true", help="start separate thread for OS-signals (try this if CTRL-C is busted)")
else:
ap2.add_argument("--sig-thr", action="store_true", default=True, help=argparse.SUPPRESS)
ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind")
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
@@ -2019,6 +2285,7 @@ def run_argparse(
fk_salt = get_salt("fk", 18)
dk_salt = get_salt("dk", 30)
ah_salt = get_salt("ah", 18)
wopi_salt = get_salt("wopi", 18)
# alpine peaks at 5 threads for some reason,
# all others scale past that (but try to avoid SMT),
@@ -2053,8 +2320,9 @@ 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_salt(ap, fk_salt, dk_salt, ah_salt, wopi_salt)
add_optouts(ap)
add_shutdown(ap)
add_yolo(ap)
@@ -2157,6 +2425,9 @@ def main(argv: Optional[list[str]] = None) -> None:
argv.pop(n)
break
if CFG_DEF:
CFG_DEF[0] = absreal(CFG_DEF[0])
ensure_locale()
ensure_webdeps()
@@ -2311,7 +2582,8 @@ def main(argv: Optional[list[str]] = None) -> None:
if al.ciphers:
configure_ssl_ciphers(al)
else:
warn("ssl module does not exist; cannot enable https")
if not al.http_only and not NO_TLS:
warn("ssl module does not exist; cannot enable https")
al.http_only = True
if PY2 and WINDOWS and al.e2d:
+2 -2
View File
@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 16)
VERSION = (1, 20, 26)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 5, 26)
BUILD_DT = (2026, 10, 14)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
+296 -29
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
@@ -13,7 +13,7 @@ import threading
import time
from datetime import datetime
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .__init__ import ANYWIN, CFG_DEF, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash
@@ -25,6 +25,7 @@ from .util import (
FN_EMB,
HAVE_SQLITE3,
IMPLICATIONS,
LOG,
META_NOBOTS,
MIMES,
SQLITE_VER,
@@ -41,6 +42,7 @@ from .util import (
odfusion,
read_utf8,
relchk,
sanitize_fn,
statdir,
ub64enc,
uncyg,
@@ -106,6 +108,7 @@ SEESLOG = " (see fileserver log for details)"
SSEELOG = " ({})".format(SEE_LOG)
BAD_CFG = "invalid config; {}".format(SEE_LOG)
SBADCFG = " ({})".format(BAD_CFG)
REDUP_E2 = "WARNING: [/%s] needs 'e2ds' for redup from 'ref' and/or to 'sym'"
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
@@ -424,6 +427,7 @@ class VFS(object):
self.root = self
self.dev = 0 # st_dev
self.nodes: dict[str, VFS] = {} # child nodes
self.lnodes: Optional[dict[bytes, VFS]] = None # lowercase
self.histtab: dict[str, str] = {} # all realpath->histpath
self.dbpaths: dict[str, str] = {} # all realpath->dbpath
self.dbv: Optional[VFS] = None # closest full/non-jump parent
@@ -443,6 +447,7 @@ class VFS(object):
self.adot: dict[str, list[str]] = {}
self.js_ls = {}
self.js_htm = ""
self.md_htm = ""
self.all_vols: dict[str, VFS] = {} # flattened recursive
self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares
self.all_fvols: dict[str, VFS] = {} # volumes which are files
@@ -465,6 +470,7 @@ class VFS(object):
self.canonical = self._canonical_null
self.dcanonical = self._dcanonical_null
self.get = self._get
self.get_dbv = self._get_dbv
self.ls = self._ls
@@ -572,6 +578,11 @@ class VFS(object):
if name in self.nodes:
return self.nodes[name]._find(rem)
if self.lnodes:
zs = name.casefold().encode("ascii", "ignore")
if zs in self.lnodes:
return self.lnodes[zs]._find(rem)
return self, vpath
def can_access(
@@ -593,7 +604,7 @@ class VFS(object):
ret += "A"
return ret
def get(
def _get(
self,
vpath: str,
uname: str,
@@ -635,6 +646,42 @@ class VFS(object):
return vn, rem
def _get_xvd(
self,
vpath: str,
uname: str,
will_read: bool,
will_write: bool,
will_move: bool = False,
will_del: bool = False,
will_get: bool = False,
err: int = 403,
) -> tuple["VFS", str]:
zvr = self._get(
vpath, uname, will_read, will_write, will_move, will_del, will_get, err
)
if not zvr:
return None
vn, rem = zvr
if "xvd" in vn.flags and vn.realpath:
avn = vn.chk_ap(vn.canonical(rem))
if avn and avn.vpath == vn.vpath:
return vn, rem
if (
not avn
or (will_read and uname not in avn.axs.uread)
or (will_write and uname not in avn.axs.uwrite)
or (will_move and uname not in avn.axs.umove)
or (will_del and uname not in avn.axs.udel)
or (will_get and uname not in avn.axs.uget)
):
if not err:
return None
raise Pebkac(
err, "permission denied for [/%s]" % (vjoin(vn.vpath, rem))
)
return vn, rem
def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
src = self.shr_src
if not src:
@@ -670,8 +717,6 @@ class VFS(object):
if lfn == zs.lower():
hit = zs
break
if not hit:
return True # NFC/NFD or something, can't be helped either way
if self.log:
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
self.log("vfs", t % (fn, hit))
@@ -762,6 +807,32 @@ class VFS(object):
real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
return abspath, real, {}
def _ls_xvd(
self,
rem: str,
uname: str,
scandir: bool,
permsets: list[list[bool]],
lstat: bool = False,
throw: bool = False,
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
dirpath, real0, virt_vis = self._ls(rem, uname, scandir, permsets, True, throw)
lns = [x for x in real0 if stat.S_ISLNK(x[1].st_mode)]
if not lns:
return dirpath, real0, virt_vis
real = [x for x in real0 if not stat.S_ISLNK(x[1].st_mode)]
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
for fn, _ in lns:
ap = absreal(os.path.join(dirpath, fn))
vn = self.chk_ap(ap)
try:
if vn.get("", uname, *ps):
real.append((fn, bos.stat(ap)))
except:
pass
real.sort()
return dirpath, real, virt_vis
def _ls(
self,
rem: str,
@@ -783,6 +854,12 @@ class VFS(object):
if not rem:
# no vfs nodes in the list of real inodes
real = [x for x in real if x[0] not in self.nodes]
if self.lnodes:
real = [
x
for x in real
if x[0].casefold().encode("ascii", "ignore") not in self.lnodes
]
dbv = self.dbv or self
for name, vn2 in sorted(self.nodes.items()):
@@ -856,12 +933,16 @@ class VFS(object):
self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3)
return
if "xdev" in self.flags or "xvol" in self.flags:
if "xvd" in self.flags:
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
rm1 = []
for le in vfs_ls:
ap = absreal(os.path.join(fsroot, le[0]))
vn2 = self.chk_ap(ap)
if not vn2 or not vn2.get("", uname, True, False, err=0):
try:
if not vn2.get("", uname, *ps):
raise Exception()
except:
rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
@@ -948,6 +1029,22 @@ class VFS(object):
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
yield d
def sanitize_fn(self, fn: str, rem: Any = None) -> str:
fn = sanitize_fn(fn, self.flags["fsnt"])
if rem:
return fn
if fn in self.nodes or (
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
):
raise Pebkac(400, "bad filename")
return fn
def chk_fn(self, fn: str) -> None:
if fn in self.nodes or (
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
):
raise Pebkac(400, "bad filename")
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
aps = ap + os.sep
if "xdev" in self.flags and not ANYWIN:
@@ -1194,7 +1291,7 @@ class AuthSrv(object):
for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
if bool(ptn.search(src)) != bool(ptn.search(dst)):
zsl.append(sigil)
if zsl:
if zsl and src != "//NULL":
t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
t = "\n".join([t % (x, src, dst) for x in zsl])
self.log(t, 1)
@@ -1274,8 +1371,16 @@ class AuthSrv(object):
daxs: dict[str, AXS],
mflags: dict[str, dict[str, Any]],
) -> tuple[str, str]:
src = os.path.expanduser(self.args.shenvexp(src))
src = absreal(src)
if src == "//NULL":
src = ""
else:
src = os.path.expanduser(self.args.shenvexp(src))
src = absreal(src)
if dst == "//NULL":
t = "//NULL given as URL instead of filesystem-path"
self.log(t, 1)
raise Exception(t)
dst = dst.strip("/")
if dst in mount:
@@ -1283,7 +1388,7 @@ class AuthSrv(object):
self.log(t.format(dst, mount[dst][0], src), c=1)
raise Exception(BAD_CFG)
if src in mount.values():
if src and src in mount.values():
t = "filesystem-path [{}] mounted in multiple locations:"
t = t.format(src)
for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
@@ -1292,7 +1397,7 @@ class AuthSrv(object):
self.log(t, c=3)
raise Exception(BAD_CFG)
if not bos.path.exists(src):
if src and not bos.path.exists(src):
self.log("warning: filesystem-path did not exist: %r" % (src,), 3)
vf = {}
@@ -1860,7 +1965,7 @@ class AuthSrv(object):
if WINDOWS:
cased = {}
for vp, (ap, vp0) in mount.items():
cased[vp] = (absreal(ap), vp0)
cased[vp] = (absreal(ap) if ap else "", vp0)
mount = cased
@@ -2030,6 +2135,15 @@ class AuthSrv(object):
umap[usr].sort()
setattr(vfs, "a" + perm, umap)
if self.args.wopi:
zsl = unames[:]
if self.args.wopi_accs:
zsl = [x.strip() for x in self.args.wopi_accs.split(",")]
axs = AXS(zsl, zsl, None, zsl)
vn = VFS(self.log_func, "", "wopi", "", axs, self.vf0())
vn.flags["unlistcr"] = vn.flags["unlistcw"] = True
vfs.nodes["wopi"] = vfs.all_nodes["wopi"] = vn
for vol in vfs.all_nodes.values():
za = vol.axs
vol.uaxs = {
@@ -2328,7 +2442,7 @@ class AuthSrv(object):
zs = vol.flags.get("rotf")
if zs:
use = True
lim.set_rotf(zs, vol.flags.get("rotf_tz") or "UTC")
lim.set_rotf(zs, vol.flags.get("rotf_tz", self.args.rotf_tz) or "UTC")
zs = vol.flags.get("maxn")
if zs:
@@ -2369,6 +2483,7 @@ class AuthSrv(object):
vol.flags["dvthumb"] = True
vol.flags["dathumb"] = True
vol.flags["dithumb"] = True
vol.flags["dethumb"] = True
have_fk = False
for vol in vfs.all_nodes.values():
@@ -2463,7 +2578,7 @@ class AuthSrv(object):
if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip gid nrand tail_who th_qv th_qvx th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
zs = "forget_ip gid md_nhist nrand tail_who th_qv th_qvx th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])
@@ -2603,6 +2718,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)
@@ -2616,6 +2738,8 @@ class AuthSrv(object):
zsl4 = list(set([x.lower() for x in zsl2]))
emb_all.update(zsl3)
emb_all.update(zsl4)
if "no_readme" in vol.flags:
zsl1 = zsl2 = zsl3 = zsl4 = []
vol.flags["emb_mds"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]]
zsl1 = [x for x in vol.flags["prologues"].split(",") if x]
@@ -2624,6 +2748,8 @@ class AuthSrv(object):
zsl4 = list(set([x.lower() for x in zsl2]))
emb_all.update(zsl3)
emb_all.update(zsl4)
if "no_logues" in vol.flags:
zsl1 = zsl2 = zsl3 = zsl4 = []
vol.flags["emb_lgs"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]]
zs = str(vol.flags.get("html_head") or "")
@@ -2640,17 +2766,23 @@ 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 = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
zs = vol.flags.get("csp_dl", "")
csp_dl = zs2 % (zs,) if zs.lower() not in ("", "no") else ""
zs = "X-Content-Type-Options: nosniff\r\n"
if "norobots" in vol.flags:
head_s += META_NOBOTS
zs += "X-Robots-Tag: noindex, nofollow\r\n"
if self.args.http_vary:
zs += "Vary: %s\r\n" % (self.args.http_vary,)
vol.flags["oh_g"] = zs + "\r\n"
vol.flags["oh_g"] = zs + csp_ui + "\r\n"
if "noscript" in vol.flags:
zs += "Content-Security-Policy: script-src 'none';\r\n"
vol.flags["oh_f"] = zs + "\r\n"
csp_dl = "Content-Security-Policy: script-src 'none';\r\n"
vol.flags["oh_f"] = zs + csp_dl + "\r\n"
ico_url = vol.flags.get("ufavico")
if ico_url:
@@ -2736,7 +2868,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 vol.vpath.startswith(shrs)):
self.log(t % (k, vol.vpath), 1)
vol.flags.pop(k)
zi = vol.flags.get("lifetime") or 0
@@ -2796,6 +2929,34 @@ class AuthSrv(object):
zs = "select %s from up where rd=? and fn=?" % (", ".join(up_q),)
vol.flags["ls_q_m"] = (zs if up_m else "", up_m)
zltss = [] # recheck
for vn1 in vfs.all_nodes.values():
if "show_hist" in vn1.flags or not vn1.realpath:
continue
ap = vn1.realpath.replace(os.sep, "/")
apS = ap.rstrip("/") + "/"
for vn2 in vfs.all_nodes.values():
if not vn2.realpath:
continue
haps = [
vn2.histpath.replace(os.sep, "/"),
vn2.dbpath.replace(os.sep, "/"),
]
for hap in list(set(haps)):
if not hap.startswith(apS):
continue
zs = hap[len(apS) :]
if "/" in zs:
zltss.append((vjoin(vn1.vpath, zs), hap))
else:
vn1.add("", zs, zs)
for vp, hap in zltss:
zss = vfs.get(vp, "", False, False)[0].axs.uget
if zss:
t = "note: /%s gives access to %r for %s"
self.log(t % (vp, hap, zss), 3)
zltss[:] = []
vfs.all_fvols = {
zs: vol for zs, vol in vfs.all_vols.items() if "is_file" in vol.flags
}
@@ -2817,12 +2978,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"
@@ -2848,6 +3010,17 @@ class AuthSrv(object):
if ccs == "y":
vol.flags["bcasechk"] = True
ptn = re.compile(r"^(,no|,sym|,hard|,ref)+=(sym|hard|ref)$")
ptn2 = re.compile(r"(no|ref).*=|=sym")
for vol in vfs.all_nodes.values():
zs = vol.flags.get("redup")
if zs and not ptn.match("," + zs):
t = "invalid redup for volume /%s: %r"
self.log(t % (vol.vpath, zs), 1)
errors = True
if zs and "e2ds" not in vol.flags and ptn2.search(zs):
self.log(REDUP_E2 % (vol.vpath,), 3)
tags = self.args.mtp or []
tags = [x.split("=")[0] for x in tags]
tags = [y for x in tags for y in x.split(",")]
@@ -2873,6 +3046,14 @@ class AuthSrv(object):
excl.extend([r"^\.", r"/\."])
vol.flags["srch_re_nodot"] = re.compile("|".join(excl))
for vol in list(vfs.all_nodes.values()):
if "xvd" in vol.flags:
for vol in vfs.all_nodes.values():
vol.get = vol._get_xvd
if vol.ls.__name__ == "_ls":
vol.ls = vol._ls_xvd
break
have_daw = False
for vol in vfs.all_nodes.values():
daw = vol.flags.get("daw") or self.args.daw
@@ -2890,6 +3071,9 @@ class AuthSrv(object):
errors = True
for vol in vfs.all_nodes.values():
for zs in vf_bmap().values():
if vol.flags.get(zs) in (None, False, ""):
vol.flags.pop(zs, None)
for k in list(vol.flags.keys()):
if re.match("^-[^-]+$", k):
vol.flags.pop(k)
@@ -2904,6 +3088,22 @@ class AuthSrv(object):
for name in vol.axs.uread:
vol.axs.udot.add(name)
zi = 1
zs = "; you must also enable --hack-me-bro to accept the total loss of security"
if self.args.hack_me_bro:
zi = 3
zs = ""
if self.args.smb:
t = "smb is enabled; this has serious security issues which will not be fixed"
self.log(t + zs, zi)
if zs:
errors = True
if self.args.tftp:
t = "tftp is enabled; primitive protocol with primitive access-restrictions"
self.log(t, 3)
if errors:
sys.exit(1)
@@ -2991,6 +3191,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"):
@@ -3001,6 +3205,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
@@ -3150,6 +3356,13 @@ class AuthSrv(object):
shn.dbpath = o_vn.dbpath
shn.histpath = o_vn.histpath
zs = "assert_root daw dk dks dky e2ds e2dsa e2ts e2tsr fk fka landmark opds rss scan th_pregen"
for zs2 in zs.split(" "):
shn.flags.pop(zs2, None)
zs = "srch_excl srch_re_dots srch_re_nodot th_coversd th_coversl"
for zs2 in zs.split(" "):
shn.flags[zs2] = []
# root.all_aps doesn't include any shares, so make a copy where the
# share appears in all abspaths it can provide (for example for chk_ap)
ap = shn.realpath
@@ -3179,6 +3392,9 @@ class AuthSrv(object):
# transplant shadowing into shares
for vn in shv.nodes.values():
svn, srem = vn.shr_src # type: ignore
if "show_hist" not in vn.flags:
zvn = VFS(self.log_func, "", "", "", AXS(), self.vf0())
vn.nodes[".hist"] = zvn
if srem:
continue # free branch, safe
ap = svn.canonical(srem)
@@ -3192,8 +3408,42 @@ class AuthSrv(object):
cur.close()
db.close()
zb1 = zb2 = False
nodes = [self.vfs]
while nodes:
vn = nodes.pop()
nodes += list(vn.nodes.values())
if (
not vn.realpath
or "bcasechk" not in vn.flags
or (not vn.axs.uread and not vn.axs.uwrite and not vn.axs.uget)
):
continue
if ".hist" in vn.nodes and "e2d" in vn.flags:
zb1 = True
for zs, vnn in vn.nodes.items():
if zs != ".hist" and (vnn.realpath or "").startswith(vn.realpath):
t = "nested volume on casefolding filesystem: [/%s] inside [/%s]"
self.log(t % (vnn.vpath, vn.vpath), 3)
zb2 = True
if PY2:
continue
vn.lnodes = {
k.casefold().encode("ascii", "ignore"): v for k, v in vn.nodes.items()
}
if zb2 and PY2:
t = "SECURITY WARNING: due to python2, shadowing will NOT provide much security:"
self.log(t, 1)
if zb2:
t = "SECURITY WARNING: you have a case-insensitive / casefolding filesystem, and you also have nested volumes (subvolumes). This combination is NOT 100% bulletproof; it *might* be possible for someone to sneakily access a subvolume they're not supposed to have access to, if they have access to the parent volume."
self.log(t, 3)
if zb1:
t = "you have a case-insensitive / casefolding filesystem, so it is recommended to set --hist C:\\copyparty-data\\ or another safe place"
self.log(t, 3)
self.js_ls = {}
self.js_htm = {}
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
@@ -3210,7 +3460,7 @@ class AuthSrv(object):
"dth3x": vf["th3x"],
"u2ts": vf["u2ts"],
"shr_who": vf["shr_who"],
"frand": bool(vf.get("rand")),
"frand": "rand" in vf,
"lifetime": vf.get("lifetime") or 0,
"unlist": vf.get("unlist") or "",
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
@@ -3268,7 +3518,7 @@ class AuthSrv(object):
"u2sz": self.args.u2sz,
"u2ts": vf["u2ts"],
"u2ow": vf["u2ow"],
"frand": bool(vf.get("rand")),
"frand": "rand" in vf,
"lifetime": vn.js_ls["lifetime"],
"u2sort": self.args.u2sort,
}
@@ -3285,7 +3535,15 @@ 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(" ")}
md_htm["modpoll_freq"] = self.args.mcr
vn.js_htm = json_hesc(json.dumps(js_htm))
vn.md_htm = json_hesc(json.dumps(md_htm))
vols = list(vfs.all_nodes.values())
if enshare:
@@ -3304,6 +3562,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("#")
@@ -3731,12 +3990,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
@@ -3863,6 +4122,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)
@@ -3872,7 +4134,7 @@ class AuthSrv(object):
except:
pass
if v2 is v:
if v2 == v:
continue
except:
pass
@@ -3916,7 +4178,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:
@@ -4004,6 +4266,11 @@ def expand_config_file(
if fp2 in ipath:
continue
if CFG_DEF and CFG_DEF[0] == fp2:
t = "ignoring PRTY_CONFIG because it was also autodiscovered by %s -> %s"
LOG[0]("root", t % (ipath, fp))
continue
expand_config_file(log, shenvexp, ret, fp2, ipath)
return
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
+8 -3
View File
@@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import division, print_function, unicode_literals
import calendar
import errno
import json
@@ -8,6 +11,7 @@ import time
from .__init__ import ANYWIN
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
NO_TLS = bool(os.environ.get("PRTY_NO_TLS"))
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
if True: # pylint: disable=using-constant-test
@@ -70,8 +74,9 @@ def ensure_cert(log: "RootLogger", args) -> None:
with open(args.cert, "rb") as f:
active_cert = f.read()
if active_cert == cert_insec:
t = "using default TLS certificate; https will be insecure:\033[36m {}"
log("cert", t.format(args.cert), 3)
t = "using default TLS certificate; %s will be insecure:\033[36m %s"
t2 = "https" if not args.ftps else "ftps"
log("cert", t % (t2, args.cert), 3)
except:
pass
@@ -252,7 +257,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
global HAVE_CFSSL
if args.http_only:
if args.http_only and not args.ftps:
return
if args.no_crt or args.certkey or not HAVE_CFSSL:
+30 -2
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"
@@ -22,6 +22,7 @@ def vf_bmap() -> dict[str, str]:
"no_dupe_m": "nodupem",
"no_forget": "noforget",
"no_html": "nohtml",
"no_mime": "nomime",
"no_pipe": "nopipe",
"no_robots": "norobots",
"no_script": "noscript",
@@ -33,6 +34,7 @@ def vf_bmap() -> dict[str, str]:
"vol_or_crash": "assert_root",
}
for k in (
"allow_svg_js",
"dedup",
"dlni",
"dothidden",
@@ -50,9 +52,12 @@ def vf_bmap() -> dict[str, str]:
"grid",
"gsel",
"hardlink",
"hist_cow",
"magic",
"md_no_br",
"no_db_ip",
"no_logues",
"no_readme",
"no_sb_md",
"no_sb_lg",
"nsort",
@@ -61,10 +66,12 @@ def vf_bmap() -> dict[str, str]:
"og_s_title",
"opds",
"rand",
"redup_dry",
"reflink",
"rm_partial",
"rmagic",
"rss",
"show_hist",
"ui_noacci",
"ui_nocpla",
"ui_nolbar",
@@ -105,6 +112,8 @@ def vf_vmap() -> dict[str, str]:
"casechk",
"chmod_d",
"chmod_f",
"csp_ui",
"csp_dl",
"dbd",
"db_xattr",
"du_who",
@@ -120,6 +129,7 @@ def vf_vmap() -> dict[str, str]:
"lg_sba",
"md_sba",
"md_hist",
"md_nhist",
"nrand",
"u2ow",
"og_desc",
@@ -137,6 +147,8 @@ def vf_vmap() -> dict[str, str]:
"put_ck",
"put_name",
"readmes",
"redup",
"rotf_tz",
"mv_retry",
"rm_retry",
"rss_sort",
@@ -150,6 +162,7 @@ def vf_vmap() -> dict[str, str]:
"tail_tmax",
"tail_who",
"tcolor",
"th_covers",
"th_pregen",
"th_qv",
"th_qvx",
@@ -218,6 +231,8 @@ flagcats = {
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
"safededup": "verify on-disk data before using it for dedup",
"redup=A,B=T": "convert dedup-type for existing files",
"redup_dry": "dry-run mode for redup",
"noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)",
"nodupem": "rejects existing files during moves as well",
@@ -289,6 +304,7 @@ flagcats = {
"no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"hist_cow": "btrfs: keep copy-on-write enabled on db/thumbs",
"xlink": "cross-volume dupe detection / linking (dangerous)",
"xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root",
@@ -308,12 +324,14 @@ flagcats = {
"dvthumb": "disables video thumbnails",
"dathumb": "disables audio thumbnails (spectrograms)",
"dithumb": "disables image thumbnails",
"dethumb": "disables custom thumbnails (--th-extract)",
"pngquant": "compress audio waveforms 33% better",
"thsize": "thumbnail res; WxH",
"crop": "center-cropping (y/n/fy/fn)",
"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",
@@ -360,6 +378,10 @@ flagcats = {
"epilogues=.epilogue.html": "files to embed below/after files",
"readmes=readme.md,README.md": "files to embed as readmes",
"preadmes=preadme.md,PREADME.md": "files to embed as preadmes",
"plainreadme": "embed readme/preadme as monospace plaintext",
"plainlogues": "embed prologue/epilogue as sans-serif plaintext",
"no_readme": "disable embedding readme/preadme",
"no_logues": "disable embedding prologue/epilogue",
"no_sb_md": "disable js sandbox for markdown files",
"no_sb_lg": "disable js sandbox for prologue/epilogue",
"sb_md": "enable js sandbox for markdown files (default)",
@@ -368,8 +390,12 @@ flagcats = {
"lg_sbf": "list of *logue-sandbox safeguards to disable",
"md_sba": "value of iframe allow-prop for markdown-sandbox",
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
"csp_ui": "content-security-policy for the web-UI",
"csp_dl": "content-security-policy for static files",
"nohtml": "return html and markdown as text/html",
"nomime": "disallow ?mime to change response type",
"noscript": "disable most javascript by use of CSP",
"allow_svg_js": "allow javascript in svg images",
"ui_noacci": "hide account-info in the UI",
"ui_nocpla": "hide cpanel-link in the UI",
"ui_nolbar": "hide link-bar in the UI",
@@ -401,7 +427,9 @@ flagcats = {
"textfiles": {
"rw_edit=md,txt": "only require read+write to edit .md and .txt",
"md_no_br": "newline only on double-newline or two tailing spaces",
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"md_hist": "where to put text-editor backups; s=subfolder, v=volHist, n=nope",
"md_nhist=99": "how many text-editor backups to keep",
"show_hist": "allow browsing the .hist folder (old textfiles)",
"exp": "enable textfile expansion; see --help-exp",
"exp_md": "placeholders to expand in markdown files; see --help",
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
+1 -1
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
+23 -3
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
@@ -10,7 +10,7 @@ import time
from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
from .authsrv import AXS, VFS, AuthSrv
from .bos import bos
from .util import chkcmd, json_hesc, min_ex, undot
from .util import chkcmd, fsenc, json_hesc, min_ex, runcmd, undot
if True: # pylint: disable=using-constant-test
from typing import Optional, Union
@@ -18,6 +18,11 @@ if True: # pylint: disable=using-constant-test
from .util import RootLogger, undot
PTN_FILEFRAG = re.compile(
r"^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):\s*[0-9]+:\s*(.*)$"
)
class Fstab(object):
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
self.log_func = log
@@ -248,7 +253,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 +268,18 @@ 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
def filefrag(ap: str) -> list[str]:
rc, so, se = runcmd([b"filefrag", b"-v", fsenc(ap)], timeout=10)
ptn = PTN_FILEFRAG
ret = []
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
a, b, s = m.groups()
if "shared" in s.strip().split(","):
ret.append((a, b))
return ret
+64 -31
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
@@ -15,7 +15,7 @@ from pyftpdlib.handlers import FTPHandler
from pyftpdlib.ioloop import IOLoop
from pyftpdlib.servers import FTPServer
from .__init__ import PY2, TYPE_CHECKING
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import (
@@ -28,7 +28,7 @@ from .util import (
fsenc,
ipnorm,
pybin,
relchk,
read_utf8,
runhook,
sanitize_fn,
set_fperms,
@@ -148,6 +148,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"
@@ -155,8 +156,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,
@@ -167,16 +168,23 @@ class FtpFs(AbstractedFS):
d: bool = False,
) -> tuple[str, VFS, str]:
try:
vpath = vpath.replace("\\", "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
logging.warning("malicious vpath: %s", vpath)
t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1)
# because:
# * ftp-spec doesn't define sep
# * windows filename limitations
# need to allow/translate illegal fn,
# but require filesystem-correct rd
if ANYWIN or ("\\" in vpath and "/" not in vpath):
vpath = vpath.replace("\\", "/")
vpath = vpath.strip("/\\")
if ANYWIN:
rd, fn = os.path.split(vpath)
fn = sanitize_fn(fn, "win")
vpath = vjoin(rd, fn)
# end of windows jank
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
vpath = vjoin(vfs.vpath, rem)
rd, fn = os.path.split(vpath)
if (
w
and fn.lower() in vfs.flags["emb_all"]
@@ -191,18 +199,7 @@ class FtpFs(AbstractedFS):
t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath))
if "xdev" in vfs.flags or "xvol" in vfs.flags:
ap = vfs.canonical(rem)
avfs = vfs.chk_ap(ap)
t = "Permission denied in [{}]"
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1)
else:
ap = vfs.canonical(rem, False)
ap = vfs.canonical(rem, False)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise FSE("No such file or directory", 1)
@@ -237,6 +234,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
@@ -254,7 +253,7 @@ class FtpFs(AbstractedFS):
xbu = vfs.flags.get("xbu")
if xbu:
hr = runhook(
self.log,
self.clog,
None,
self.hub.up2k,
"xbu.ftp",
@@ -274,7 +273,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
@@ -299,17 +298,22 @@ 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:
nwd = join(self.cwd, path)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
try:
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
except Exception as ex:
raise FSE(str(ex))
if not vfs.realpath:
self.cwd = nwd
return
@@ -330,10 +334,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)
@@ -383,6 +389,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)
@@ -391,6 +398,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")
@@ -401,6 +409,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")
@@ -430,6 +439,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)
@@ -437,6 +447,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)
@@ -479,9 +492,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
@@ -510,6 +527,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] = {}
@@ -517,6 +535,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("/")
@@ -529,7 +556,7 @@ class FtpHandler(FTPHandler):
xbu = vfs.flags.get("xbu")
if xbu:
hr = runhook(
None,
self.clog,
None,
self.hub.up2k,
"xbu.ftpd",
@@ -624,11 +651,17 @@ class Ftpd(object):
hs.append([h1, self.args.ftps])
zs = self.args.ftp_banner.replace("\\n", "\n")
if zs.startswith("@"):
zs = read_utf8(None, zs[1:], False)
banner = zs.replace("\r", "").replace("\n", "\r\n").strip()
for h_lp in hs:
h2, lp = h_lp
FtpHandler.hub = h2.hub = hub
FtpHandler.args = h2.args = hub.args
FtpHandler.authorizer = h2.authorizer = FtpAuth(hub)
FtpHandler.banner = banner
if self.args.ftp_pr:
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]
+444 -98
View File
File diff suppressed because it is too large. Load diff
+12 -11
View File
@@ -1,29 +1,30 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import argparse # typechk
import os
import re
import socket
import threading # typechk
import time
try:
if os.environ.get("PRTY_NO_TLS"):
raise Exception()
HAVE_SSL = True
import ssl
except:
HAVE_SSL = False
from . import util as Util
from .__init__ import TYPE_CHECKING, EnvParams
from .authsrv import AuthSrv # typechk
from .cert import NO_TLS
from .httpcli import HttpCli
from .u2idx import U2idx
from .util import HMaccas, NetMap, min_ex, shut_socket
try:
if NO_TLS:
raise Exception()
import ssl
HAVE_SSL = True
except:
HAVE_SSL = False
if True: # pylint: disable=using-constant-test
from typing import Optional, Pattern, Union
+5 -3
View File
@@ -1,8 +1,7 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import hashlib
import math
import os
import re
import socket
@@ -180,6 +179,8 @@ class HttpSrv(object):
self.u2idx_free: dict[str, U2idx] = {}
self.u2idx_n = 0
self.wopi_files: dict[str, dict[str, Any]] = {}
assert jinja2 # type: ignore # !rm
env = jinja2.Environment()
env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
@@ -195,6 +196,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 +317,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),
+1 -1
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
+1 -2
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
@@ -51,7 +51,6 @@ except ImportError:
SRV,
TXT,
A,
Bimap,
DNSHeader,
DNSQuestion,
DNSRecord,
+1 -1
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
+53 -17
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
@@ -15,8 +15,11 @@ from .authsrv import VFS
from .bos import bos
from .util import (
FFMPEG_URL,
LOG,
REKOBO_LKEY,
SCWD,
VF_CAREFUL,
absreal,
expand_osenv_c,
fsenc,
gzip,
@@ -27,6 +30,9 @@ from .util import (
sfsenc,
uncyg,
wunlink,
zip_fi,
zip_lim,
zip_readf,
)
if True: # pylint: disable=using-constant-test
@@ -46,10 +52,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:
@@ -65,15 +71,17 @@ 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")
HAVE_FFPROBE = have_ff("ffprobe")
TH_BWRAP = []
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split())
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
@@ -169,7 +177,7 @@ def au_unpk(
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
if not zil:
raise Exception("no audio inside zip")
fi = zf.open(zil[0])
fi = zf.open(zip_lim(zil[0], maxsz))
elif pk == "cbz":
import zipfile
@@ -183,11 +191,18 @@ def au_unpk(
t = "cbz: %d files, %d hits" % (nf, len(znil))
if not znil:
raise Exception("no images inside cbz")
using = sorted(znil)[0][1].filename
zi = sorted(znil)[0][1]
if znil:
t += ", using " + using
t += ", using " + zi.filename
log(t)
fi = zf.open(using)
fi = zf.open(zip_lim(zi, maxsz))
elif pk == "kra" or pk == "ora":
import zipfile
zf = zipfile.ZipFile(abspath, "r")
zi = zip_fi(zf, "mergedimage.png", maxsz)
fi = zf.open(zi)
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
@@ -224,19 +239,37 @@ def au_unpk(
return abspath
def bwrap(prog: bytes, ap_in: bytes, ap_out: bytes) -> list[bytes]:
if not TH_BWRAP:
return [prog]
ret = TH_BWRAP + [b"--ro-bind", prog, prog, b"--ro-bind", ap_in, ap_in]
if ap_out:
zs = ap_out.rsplit(b"/", 1)[0]
ret += [b"--bind", zs, zs]
ret.append(prog)
return ret
def bwrap_fail(serr: str) -> None:
if "bwrap:" in serr:
LOG[0]("root", "ffmpeg failed due to --th-bwrap;\n " + serr, 3)
def ffprobe(
abspath: str, timeout: int = 60
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
# ffprobe -hide_banner -show_streams -show_format --
cmd = [
HAVE_FFPROBE,
bap = fsenc(abspath)
cmd = bwrap(HAVE_FFPROBE[0], bap, b"") + [
b"-hide_banner",
b"-show_streams",
b"-show_format",
b"--",
fsenc(abspath),
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)
return parse_ffprobe(so)
@@ -409,7 +442,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
with zipfile.ZipFile(abspath, "r") as z:
# First open the container file to find the package document (.opf file)
try:
container_root = parse_xml(z.read("META-INF/container.xml").decode())
zb = zip_readf(z, "META-INF/container.xml", 2 << 20)
container_root = parse_xml(zb.decode())
except KeyError:
log("epub: no container file found in %s" % (abspath,))
return None
@@ -423,7 +457,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
# Then open the first package document to find the path of the cover image
try:
package_root = parse_xml(z.read(rootfile_path).decode())
zb = zip_readf(z, rootfile_path, 2 << 20)
package_root = parse_xml(zb.decode())
except KeyError:
log("epub: no package document found in %s" % (abspath,))
return None
@@ -447,7 +482,8 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
try:
return z.open(adjusted_cover_path)
zi = zip_fi(z, adjusted_cover_path, 2 << 24)
return z.open(zi)
except KeyError:
t = "epub: cover specified in package document, but doesn't exist: %s"
log(t % (adjusted_cover_path,))
+1 -1
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
+3 -2
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
@@ -14,8 +14,9 @@ try:
if os.environ.get("PRTY_NO_ARGON2"):
raise Exception()
HAVE_ARGON2 = True
from argon2 import exceptions as argon2ex
HAVE_ARGON2 = True
except:
HAVE_ARGON2 = False
+1 -1
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
+90 -36
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
@@ -8,7 +8,6 @@ import os
import select
import socket
import time
from threading import ExceptHookArgs
import paramiko
import paramiko.common
@@ -21,6 +20,7 @@ from paramiko.sftp import (
SFTP_OP_UNSUPPORTED,
SFTP_PERMISSION_DENIED,
)
from queue import Queue
from .__init__ import ANYWIN, TYPE_CHECKING
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
@@ -37,7 +37,6 @@ from .util import (
ipnorm,
min_ex,
read_utf8,
relchk,
runhook,
sanitize_fn,
ub64enc,
@@ -55,6 +54,23 @@ if True: # pylint: disable=using-constant-test
SATTR = paramiko.sftp_attr.SFTPAttributes
try:
from paramiko.rsakey import serialization
_load_der_pk = serialization.load_der_private_key
def fastloader(*a, **ka):
ka0 = ka.copy()
try:
ka["unsafe_skip_rsa_key_validation"] = True
return _load_der_pk(*a, **ka)
except:
return _load_der_pk(*a, **ka0)
except:
pass
class SSH_Srv(paramiko.ServerInterface):
def __init__(self, hub: "SvcHub", addr: Any):
self.hub = hub
@@ -295,6 +311,17 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
if self.uname == LEELOO_DALLAS:
raise Exception("send her back")
cm = 0
for zs, zi in (
("s", SATTR.FLAG_SIZE),
("t", SATTR.FLAG_AMTIME),
("p", SATTR.FLAG_PERMISSIONS),
("u", SATTR.FLAG_UIDGID),
):
if zs in self.args.sftp_chattr:
cm |= zi
self.chattr_mask = cm
self.vols = [
vp
for vp, vn in self.asrv.vfs.all_vols.items()
@@ -320,15 +347,23 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
m: bool = False,
d: bool = False,
) -> tuple[str, VFS, str]:
vpath = vpath.replace(os.sep, "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
self.log("malicious vpath: %s", vpath)
raise Exception("Unsupported characters in [%s]" % (vpath,))
# because:
# * maybe some clients use \
# * windows filename limitations
# need to allow/translate illegal fn,
# but require filesystem-correct rd
if ANYWIN or ("\\" in vpath and "/" not in vpath):
vpath = vpath.replace("\\", "/")
vpath = vpath.strip("/\\")
if ANYWIN:
rd, fn = os.path.split(vpath)
fn = sanitize_fn(fn, "win")
vpath = vjoin(rd, fn)
# end of windows jank
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
vpath = vjoin(vn.vpath, rem)
rd, fn = os.path.split(vpath)
if (
w
and fn.lower() in vn.flags["emb_all"]
@@ -343,18 +378,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
# return "", vn, rem
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
if "xdev" in vn.flags or "xvol" in vn.flags:
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
t = "Permission denied in [{}]"
if not avn:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
else:
ap = vn.canonical(rem, False)
ap = vn.canonical(rem, False)
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
@@ -495,8 +519,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
smode = "rb"
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr)
ap = vn.canonical(rem, False)
ap, vn, rem = self.v2a(vp, rd, wr)
vf = vn.flags
except Pebkac as ex:
t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s"
@@ -668,8 +691,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _mkdir(self, vp: str, attr: SATTR) -> int:
self.log("mkdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
ap = vn.canonical(rem, False)
ap, vn, _ = self.v2a(vp, w=True)
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
if attr is not None:
paramiko.SFTPServer.set_file_attr(ap, attr)
@@ -692,8 +714,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _rmdir(self, vp: str) -> int:
self.log("rmdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
ap = os.path.join(vn.realpath, rem)
ap, _, _ = self.v2a(vp, d=True)
bos.rmdir(ap)
return SFTP_OK
except Pebkac as ex:
@@ -714,8 +735,8 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _chattr(self, vp: str, attr: SATTR) -> int:
self.log("chattr(%s, %s)" % (vp, attr))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
ap = os.path.join(vn.realpath, rem)
attr._flags &= self.chattr_mask
ap, _, _ = self.v2a(vp, w=True, d=True)
paramiko.SFTPServer.set_file_attr(ap, attr)
return SFTP_OK
except Pebkac as ex:
@@ -736,6 +757,22 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
return "/%s" % (undot(path),)
class Transport2(paramiko.Transport):
def __init__(self, cli, addr):
self.addr = addr
super(Transport2, self).__init__(cli)
def _check_banner(self):
try:
return super()._check_banner()
except Exception as ex:
if "Error reading SSH protocol bann" in str(ex):
self.logger.warning("%s rejected: %s" % (self.addr, ex))
self.addr = None
else:
raise
class Sftpd(object):
def __init__(self, hub: "SvcHub") -> None:
self.hub = hub
@@ -743,6 +780,7 @@ class Sftpd(object):
self.log_func = hub.log
self.srv: list[socket.socket] = []
self.bound: list[str] = []
self.hs_q = Queue()
self.sessions = {}
ips = args.sftp_i
@@ -758,6 +796,10 @@ class Sftpd(object):
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
return
if args.sftp_fastldr:
serialization.load_der_private_key = fastloader # type: ignore
else:
self.log("loading hostkeys...")
self.hostkeys = []
hostkeytypes = (
("ed25519", "Ed25519Key", {}), # best
@@ -785,6 +827,8 @@ class Sftpd(object):
self.hostkeys.append(pkey)
if args.sftpv:
self.log("loaded hostkey %r" % (pkey,))
if args.sftp_fastldr:
serialization.load_der_private_key = _load_der_pk # type: ignore
ips = list(ODict.fromkeys(ips)) # dedup
@@ -793,6 +837,9 @@ class Sftpd(object):
self.log("listening @ %s port %s" % (self.bound, args.sftp))
for n in range(self.args.sftp_hs_n):
Daemon(self.acceptor, "sftp-hs-%d" % (n,))
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log("sftp", msg, c)
@@ -828,12 +875,18 @@ class Sftpd(object):
cli, addr = srv.accept()
# cli.settimeout(0) # == srv.setblocking(False)
self.log("%r is connecting" % (addr,))
zs = "sftp-%s" % (addr[0],)
# Daemon(self._accept2, zs, (cli, addr))
self._accept2(cli, addr)
self.hs_q.put((cli, addr))
def acceptor(self) -> None:
while True:
try:
cli, addr = self.hs_q.get()
self._accept2(cli, addr)
except:
time.sleep(1)
def _accept2(self, cli, addr) -> None:
tra = paramiko.Transport(cli)
tra = Transport2(cli, addr)
for hkey in self.hostkeys:
tra.add_server_key(hkey)
tra.set_subsystem_handler("sftp", paramiko.SFTPServer, SFTP_Srv)
@@ -841,11 +894,12 @@ class Sftpd(object):
try:
tra.start_server(server=psrv)
except Exception as ex:
self.log("%r could not establish connection: %r" % (addr, ex), 3)
if tra.addr:
self.log("%r could not establish connection: %r" % (addr, ex), 3)
cli.close()
return
chan = tra.accept()
chan = tra.accept(timeout=self.args.sftp_hs_t)
if chan is None:
self.log("%r did not open an sftp channel" % (addr,), 3)
cli.close()
+1 -1
View File
@@ -12,7 +12,7 @@ from types import SimpleNamespace
from .__init__ import ANYWIN, EXE, TYPE_CHECKING
from .authsrv import LEELOO_DALLAS, VFS
from .bos import bos
from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
from .util import Daemon, absreal, min_ex, pybin, runhook
if True: # pylint: disable=using-constant-test
from typing import Any, Union
+1 -2
View File
@@ -1,11 +1,10 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import re
import select
import socket
import time
from .__init__ import TYPE_CHECKING
from .multicast import MC_Sck, MCast
+1 -1
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
+8 -1
View File
@@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import ctypes
import sys
from ctypes import wintypes
if True: # pylint: disable=using-constant-test
@@ -9,6 +10,9 @@ if True: # pylint: disable=using-constant-test
from . import _shared as shared
WINVER = sys.getwindowsversion()
LE_XP = WINVER.major < 6
NO_ERROR = 0
ERROR_BUFFER_OVERFLOW = 111
MAX_ADAPTER_NAME_LENGTH = 256
@@ -83,7 +87,10 @@ def enumerate_interfaces_of_adapter(
t = "sockaddr_to_ip({}) returned None"
raise Exception(t.format(address.Address.lpSockaddr))
network_prefix = address.OnLinkPrefixLength
if LE_XP:
network_prefix = 16 if str(ip).startswith("169.254") else 24
else:
network_prefix = address.OnLinkPrefixLength
yield shared.IP(ip, network_prefix, nice_name)
+1 -1
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
+163 -66
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
@@ -27,13 +27,23 @@ if True: # pylint: disable=using-constant-test
import typing
from typing import Any, Optional, Union
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .__init__ import (
ANYWIN,
EXE,
MACOS,
PY2,
TYPE_CHECKING,
UNIX,
E,
EnvParams,
unicode,
)
from .__version__ import S_VERSION, VERSION
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who
from .bos import bos
from .cert import ensure_cert
from .fsutil import ramdisk_chk
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN, TH_BWRAP
from .pwhash import HAVE_ARGON2
from .sutil import close_pools as sutil_close_pools
from .tcpsrv import TcpSrv
@@ -42,8 +52,6 @@ from .th_srv import (
H_PIL_HEIF,
H_PIL_WEBP,
HAVE_DCRAW,
HAVE_FFMPEG,
HAVE_FFPROBE,
HAVE_PIL,
HAVE_RAWPY,
HAVE_VIPS,
@@ -51,10 +59,12 @@ from .th_srv import (
)
from .up2k import Up2k
from .util import (
BLOCK_SIGS,
DEF_EXP,
DEF_MTE,
DEF_MTH,
FFMPEG_URL,
HAVE_BWRAP,
HAVE_PSUTIL,
HAVE_SQLITE3,
HAVE_ZMQ,
@@ -122,6 +132,19 @@ VER_SHARES_DB = 2
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
def _build_th_fset(f: str, fadd: str) -> set[str]:
fset = {x for x in f.replace(" ", "").split(",")}
rmset = {""}
for x in fadd.replace(" ", "").split(","):
if x.startswith("-"):
rmset.add(x.lstrip("-"))
else:
fset.add(x)
return fset - rmset
class SvcHub(object):
"""
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
@@ -151,6 +174,7 @@ class SvcHub(object):
self.is_dut = False # running in unittest; always False
self.stopping = False
self.stopped = False
self.init_mutex = threading.Lock()
self.reload_mutex = threading.Lock()
self.retcode = 0
self.httpsrv_up = 0
@@ -223,9 +247,14 @@ class SvcHub(object):
self.lo1 = self.lo2 = ""
if args.lo:
if "%" in args.lo and "%R" not in args.lo:
do_xz = args.lo.replace("%R", "").lower().endswith(".xz")
if "%R" not in args.lo:
args.lo += "%R"
if not args.rlo:
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")
@@ -385,6 +414,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()
@@ -422,6 +454,20 @@ class SvcHub(object):
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
args.au_unpk = {x[0]: x[1] for x in zlss}
args.th_r_pil = _build_th_fset(args.th_r_pil, args.th_pil_add)
args.th_r_vips = _build_th_fset(args.th_r_vips, args.th_vips_add)
args.th_r_raw = _build_th_fset(args.th_r_raw, args.th_raw_add)
args.th_r_ffi = _build_th_fset(args.th_r_ffi, args.th_ffi_add)
args.th_r_ffv = _build_th_fset(args.th_r_ffv, args.th_ffv_add)
args.th_r_ffa = _build_th_fset(args.th_r_ffa, args.th_ffa_add)
th_extract = args.th_extract or []
args.th_extract = {}
for arg in th_extract:
exts, script = arg.split("=", 1)
for ext in exts.split(","):
args.th_extract[ext.strip().lower()] = script
self.args.th_dec = list(decs.keys())
self.thumbsrv = None
want_ff = False
@@ -459,12 +505,14 @@ class SvcHub(object):
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
raise Exception(t % (args.q_mp3,))
else:
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
zss = args.th_r_ffa | args.th_r_ffv
args.au_unpk = {
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
}
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
if not args.th_clean:
args.th_poke = 0
zms = ""
if not args.https_only:
@@ -488,9 +536,6 @@ class SvcHub(object):
self.tftpd: Optional[Tftpd] = None
if args.sftp or args.ftp or args.ftps or args.tftp:
Daemon(self.start_ftpd, "start_tftpd")
if args.smb:
# impacket.dcerpc is noisy about listen timeouts
sto = socket.getdefaulttimeout()
@@ -814,29 +859,19 @@ class SvcHub(object):
cur.close()
db.close()
def start_ftpd(self) -> None:
time.sleep(30)
if hasattr(self, "sftpd") and not self.sftpd:
self.restart_sftpd()
if hasattr(self, "ftpd") and not self.ftpd:
self.restart_ftpd()
if hasattr(self, "tftpd") and not self.tftpd:
self.restart_tftpd()
def restart_sftpd(self) -> None:
if not hasattr(self, "sftpd"):
return
from .sftpd import Sftpd
if self.sftpd:
return # todo
with self.init_mutex:
if self.sftpd:
return # todo
self.sftpd = Sftpd(self)
self.sftpd.run()
self.sftpd = Sftpd(self)
self.sftpd.run()
self.log("root", "started SFTPd")
def restart_ftpd(self) -> None:
@@ -845,13 +880,15 @@ class SvcHub(object):
from .ftpd import Ftpd
if self.ftpd:
return # todo
with self.init_mutex:
if self.ftpd:
return # todo
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
self.ftpd = Ftpd(self)
self.ftpd = Ftpd(self)
self.log("root", "started FTPd")
def restart_tftpd(self) -> None:
@@ -860,10 +897,11 @@ class SvcHub(object):
from .tftpd import Tftpd
if self.tftpd:
return # todo
with self.init_mutex:
if self.tftpd:
return # todo
self.tftpd = Tftpd(self)
self.tftpd = Tftpd(self)
def thr_httpsrv_up(self) -> None:
time.sleep(1 if self.args.ign_ebind_all else 5)
@@ -1008,17 +1046,32 @@ 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 = []
t_ff = "transcode audio, create spectrograms, video thumbnails"
# 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"),
(HAVE_BWRAP, "bwrap", "sandbox to make ffmpeg less dangerous", not ANYWIN and not UNIX),
(HAVE_MUTAGEN, "mutagen", "read audio tags (ffprobe is better but slower)"),
(HAVE_ARGON2, "argon2", "secure password hashing (advanced users only)"),
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
@@ -1026,17 +1079,21 @@ class SvcHub(object):
(H_PIL_AVIF, "pillow-avif", "read .avif pics with pillow (rarely useful)"),
(HAVE_RAWPY, "rawpy", "read RAW images"),
(HAVE_DCRAW, "libraw", "read RAW images"),
(HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)", ANYWIN),
]
if ANYWIN:
to_check += [
(HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)")
]
# fmt: on
verbose = self.args.deps
if verbose:
self.log("dependencies", "")
for have, feat, what in to_check:
for zc in to_check:
try:
have, feat, what = zc
except:
have, feat, what, zb = zc
if not zb:
continue
lst = fok if have else fng
lst.append((feat, what))
if verbose:
@@ -1087,14 +1144,23 @@ 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 al.wopi and not al.wopi_url and not al.wopi_urls:
self.log("root", "disabling wopi; need either wopi-url or wopi-urls", 1)
al.wopi = False
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
@@ -1163,13 +1229,16 @@ 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:
al.th_bwrap = ""
elif zs == "n":
al.th_bwrap = ""
elif zs == "f":
al.th_bwrap = al.th_bwrap or "bwrap-not-available"
else:
raise Exception("--use-bwrap must be a/n/f")
for k in "c".split(" "):
if self.args.env_expand in (0, 2):
@@ -1203,7 +1272,16 @@ class SvcHub(object):
vsa = [x.upper() for x in vsa if x]
setattr(al, k + "_set", set(vsa))
zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
zs = "th_bwrap"
for k in zs.split(" "):
zsl = [x for x in str(getattr(al, k, "")).split(" ") if x]
zbl = [x.encode("ascii", "replace") for x in zsl]
setattr(al, k + "_s", zsl)
setattr(al, k + "_b", zbl)
TH_BWRAP[:] = al.th_bwrap_b
zs = "acao_re dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
for k in zs.split(" "):
vs = getattr(al, k)
if not vs or vs == "no":
@@ -1326,18 +1404,20 @@ class SvcHub(object):
self.args.name_html = zs
zs = al.u2sz
if zs in ("no", "1,1,1"):
zs = "0"
zsl = [x.strip() for x in zs.split(",")]
if len(zsl) not in (1, 3):
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
if len(zsl) not in (1, 2):
t = "invalid --u2sz; must be either one number, or a comma-separated list of two numbers (target,max)"
raise Exception(t)
if len(zsl) < 3:
zsl = ["1", zs, zs]
zi2 = 1
if len(zsl) < 2:
zsl = [zs, zs]
zi2 = 0
for zs in zsl:
zi = int(zs)
# arbitrary constraint (anything above 2 GiB is probably unintended)
if zi < 1 or zi > 2047:
raise Exception("invalid --u2sz; minimum is 1, max is 2047")
if zi < 0 or zi > 2047:
raise Exception("invalid --u2sz; minimum is 0, max is 2047")
if zi < zi2:
raise Exception("invalid --u2sz; values must be equal or ascending")
zi2 = zi
@@ -1406,7 +1486,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)
@@ -1422,18 +1502,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)
@@ -1448,6 +1541,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="")
@@ -1486,6 +1581,8 @@ class SvcHub(object):
for sig in sigs:
signal.signal(sig, self.signal_handler)
if sig not in BLOCK_SIGS and BLOCK_SIGS:
BLOCK_SIGS.append(sig)
if self.args.sig_thr:
Daemon(self._signal_thr, "svchub-sig")
+2 -2
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
@@ -104,7 +104,7 @@ def gen_hdr(
ret += spack(b"<LL", vsz, vsz)
# windows support (the "?" replace below too)
fn = sanitize_to(fn, VPTL_WIN)
fn = sanitize_to(fn, VPTL_WIN, "win")
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
+4 -2
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
@@ -22,7 +22,6 @@ from .util import (
Netdev,
atomic_move,
chkcmd,
get_adapters,
list_nics,
min_ex,
sunpack,
@@ -88,6 +87,9 @@ class TcpSrv(object):
successful_binds = 0
try:
for ipa in ips:
if ipa == "no":
self.args.ign_ebind_all = True
continue
try:
self._listen(ipa, port)
ok[ipa].append(port)
+6 -2
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
@@ -275,7 +275,11 @@ class Tftpd(object):
if not vfs.realpath:
raise Exception("unmapped vfs")
return vfs, vpath, vfs.canonical(rem)
ap = vfs.canonical(rem)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise Exception("file not found")
return vfs, vpath, ap
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
# generate file listing if vpath is dir.txt and return as file object
+19 -11
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
@@ -31,9 +31,10 @@ class ThumbCli(object):
self.asrv = hsrv.asrv
# cache on both sides for less broker spam
self.cooldown = Cooldown(self.args.th_poke)
self.cooldown = Cooldown(self.args.th_poke) if self.args.th_poke else None
self.thumbable = c["thumbable"]
self.thumbable_native = c["thumbable_native"]
self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
@@ -59,6 +60,10 @@ class ThumbCli(object):
if is_vid and "dvthumb" in dbv.flags:
return None
is_custom = ext not in self.thumbable_native
if is_custom and "dethumb" in dbv.flags:
return None
want_opus = fmt in EXTS_AC
is_au = ext in self.fmt_ffa
is_vau = want_opus and ext in self.fmt_ffv
@@ -76,7 +81,7 @@ class ThumbCli(object):
elif want_opus:
return None
is_img = not is_vid and not is_au
is_img = not is_vid and not is_au and not is_custom
if is_img and "dithumb" in dbv.flags:
return None
@@ -97,7 +102,9 @@ class ThumbCli(object):
):
sfmt = "j"
if sfmt == "x" and not self.can_jxl:
if sfmt == "x" and (
not self.can_jxl or (not H_PIL_JXL and ext in self.fmt_raw)
):
sfmt = "w"
vf_crop = dbv.flags["crop"]
@@ -149,14 +156,15 @@ class ThumbCli(object):
pass
if ret:
tdir = os.path.dirname(tpath)
if self.cooldown.poke(tdir):
self.broker.say("thumbsrv.poke", tdir)
if self.cooldown:
tdir = os.path.dirname(tpath)
if self.cooldown.poke(tdir):
self.broker.say("thumbsrv.poke", tdir)
if want_opus:
# audio files expire individually
if self.cooldown.poke(tpath):
self.broker.say("thumbsrv.poke", tpath)
if want_opus:
# audio files expire individually
if self.cooldown.poke(tpath):
self.broker.say("thumbsrv.poke", tpath)
return ret
+201 -95
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
@@ -18,16 +18,27 @@ from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .authsrv import VFS
from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe, have_ff
from .mtag import (
HAVE_FFMPEG,
HAVE_FFPROBE,
TH_BWRAP,
au_unpk,
bwrap,
bwrap_fail,
ffprobe,
have_ff,
)
from .util import BytesIO # type: ignore
from .util import (
FFMPEG_URL,
SCWD,
VF_CAREFUL,
Cooldown,
Daemon,
afsenc,
atomic_move,
fsenc,
loadpy,
min_ex,
runcmd,
statdir,
@@ -190,11 +201,11 @@ try:
# reduces glibc RAM usage from 4.7 to 3.5 GiB ...yep, still bonkers
os.environ["VIPS_CONCURRENCY"] = "1"
HAVE_VIPS = True
import pyvips
pyvips.cache_set_max(0)
logging.getLogger("pyvips").setLevel(logging.WARNING)
HAVE_VIPS = True
except Exception as e:
HAVE_VIPS = False
if not isinstance(e, ImportError):
@@ -208,10 +219,10 @@ try:
if PRTY_NO_RAWPY:
raise Exception()
HAVE_RAWPY = True
import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING)
HAVE_RAWPY = True
except:
HAVE_RAWPY = False
@@ -264,7 +275,7 @@ class ThumbSrv(object):
self.log = self._log
self.nextlog = 0
self.poke_cd = Cooldown(self.args.th_poke)
self.poke_cd = Cooldown(self.args.th_poke) if self.args.th_poke else None
self.mutex = threading.Lock()
self.busy: dict[str, list[threading.Condition]] = {}
@@ -313,24 +324,12 @@ class ThumbSrv(object):
if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln")
(
self.fmt_pil,
self.fmt_vips,
self.fmt_raw,
self.fmt_ffi,
self.fmt_ffv,
self.fmt_ffa,
) = [
set(y.split(","))
for y in [
self.args.th_r_pil,
self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi,
self.args.th_r_ffv,
self.args.th_r_ffa,
]
]
self.fmt_pil = self.args.th_r_pil
self.fmt_vips = self.args.th_r_vips
self.fmt_raw = self.args.th_r_raw
self.fmt_ffi = self.args.th_r_ffi
self.fmt_ffv = self.args.th_r_ffv
self.fmt_ffa = self.args.th_r_ffa
if not H_PIL_HEIF:
for f in "heif heifs heic heics".split(" "):
@@ -349,23 +348,26 @@ class ThumbSrv(object):
self.fmt_pil.discard(f)
self.thumbable: set[str] = set()
self.thumbable_native: set[str] = set()
self._build_thumbable()
def _build_thumbable(self) -> None:
self.thumbable.clear()
self.thumbable_native.clear()
if "pil" in self.args.th_dec:
self.thumbable |= self.fmt_pil
self.thumbable_native |= self.fmt_pil
if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips
self.thumbable_native |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
self.thumbable_native |= self.fmt_raw
if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss
self.thumbable_native |= zss
self.thumbable = self.thumbable_native | set(self.args.th_extract)
def _log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumb", msg, c)
@@ -457,6 +459,7 @@ class ThumbSrv(object):
def getcfg(self) -> dict[str, set[str]]:
return {
"thumbable": self.thumbable,
"thumbable_native": self.thumbable_native,
"pil": self.fmt_pil,
"vips": self.fmt_vips,
"raw": self.fmt_raw,
@@ -539,19 +542,36 @@ class ThumbSrv(object):
png_ok = False
funs = []
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus or want_flac or want_wav
ap_extr = abspath
if (
ext in self.args.th_extract
and not want_au
and not want_png
and "dethumb" not in vn.flags
):
ap_extr = self.run_extractor(self.args.th_extract[ext], abspath, vn)
if ap_extr:
ext = ap_extr.rsplit(".", 1)[-1]
if ext in self.args.au_unpk:
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
ap_unpk = au_unpk(self.log, self.args.au_unpk, ap_extr, vn)
elif ext in self.thumbable_native:
ap_unpk = ap_extr
else:
ap_unpk = abspath
ap_unpk = ""
if ap_extr and ap_extr != ap_unpk and ap_extr != abspath:
wunlink(self.log, ap_extr, vn.flags)
if ap_unpk and not bos.path.exists(tpath):
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus or want_flac or want_wav
for lib in self.args.th_dec:
can_au = lib == "ff" and (
ext in self.fmt_ffa or ext in self.fmt_ffv
@@ -659,6 +679,66 @@ class ThumbSrv(object):
with self.mutex:
self.nthr -= 1
def run_extractor(self, extr: str, abspath: str, vn: VFS) -> str:
try:
mod = loadpy(extr, self.args.hot_th_extr)
except Exception as ex:
self.log("extractor import failed; " + min_ex(), 1)
return ""
fd = 0
ret = ""
stream = None
try:
res = mod.main(abspath, vn=vn, th_srv=self)
if not res:
t = "extractor %r returned no data for file %r"
self.log(t % (extr, abspath))
return ""
outext, stream, offset, whence, size = res
if (
outext not in self.thumbable_native
or outext in self.fmt_ffa
or outext in self.fmt_ffv
):
t = "extractor %r returned unsupported format %r"
self.log(t % (extr, outext))
return ""
remains = size if size is not None and size >= 0 else -1
stream.seek(offset, whence)
fd, ret = tempfile.mkstemp("." + outext)
bufsz = rsz = self.args.iobuf
maxsz = self.args.th_extr_sz * 1048576
outsz = 0
with os.fdopen(fd, "wb", rsz) as f:
fd = 0
while True:
if remains < rsz and remains >= 0:
bufsz = remains
buf = stream.read(bufsz)
if not buf:
break
remains -= len(buf)
outsz += len(buf)
if outsz >= maxsz:
raise Exception("too large")
f.write(buf)
return ret
except Exception as e:
if fd:
os.close(fd)
if ret:
wunlink(self.log, ret, vn.flags)
t = "failed to extract thumbnail from %r: %s"
self.log(t % (abspath, min_ex()))
return ""
finally:
if stream:
stream.close()
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
# exif_transpose is expensive (loads full image + unconditional copy)
res = self.getres(vn, fmt)
@@ -763,17 +843,17 @@ class ThumbSrv(object):
def _conv_dcraw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.6, tpath)
bap = fsenc(abspath)
# fmt: off
cmd = [
b"dcraw_emu",
cmd = bwrap(HAVE_DCRAW[0], bap, b"") + [
b"-h", # halfsize
b"-o", b"1", # srgb
b"-s", b"0", # first frame
b"-Z", b"-", # to stdout
fsenc(abspath),
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)
@@ -824,7 +904,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
@@ -832,6 +912,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] = []
@@ -858,16 +945,15 @@ class ThumbSrv(object):
res = self.getres(vn, fmt)
bscale = scale.format(*list(res)).encode("utf-8")
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner"
]
cmd += seek
cmd += [
b"-i", fsenc(abspath),
] + seek + [
b"-i", bap_in,
b"-map", imap,
b"-vf", bscale,
b"-frames:v", b"1",
@@ -875,15 +961,15 @@ class ThumbSrv(object):
]
# fmt: on
self._ffmpeg_im_o(tpath, vn, cmd)
self._ffmpeg_im_o(bap_out, vn, cmd)
def _ffmpeg_im_o(self, tpath: str, vn: VFS, cmd: list[bytes]) -> None:
if tpath.endswith(".jpg"):
def _ffmpeg_im_o(self, tpath: bytes, vn: VFS, cmd: list[bytes]) -> None:
if tpath.endswith(b".jpg"):
cmd += [
b"-q:v",
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
elif tpath.endswith(".jxl"):
elif tpath.endswith(b".jxl"):
cmd += [
b"-q:v",
unicode(vn.flags["th_qvx"]).encode("ascii"), # default=??
@@ -898,15 +984,18 @@ class ThumbSrv(object):
b"6", # default=4, 0=fast, 6=max
]
cmd += [fsenc(tpath)]
cmd.append(tpath)
self._run_ff(cmd, vn, "convt")
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
if TH_BWRAP:
bwrap_fail(serr)
c: Union[str, int] = "90"
t = "FFmpeg failed (probably a corrupt file):\n"
@@ -998,20 +1087,21 @@ class ThumbSrv(object):
b",showwavespic=s=2048x64:colors=white"
b",convolution=1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 -1 1 -1 5 -1 1 -1 1" # idk what im doing but it looks ok
)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
b"-filter_complex", flt,
b"-frames:v", b"1",
]
# fmt: on
cmd += [fsenc(tpath)]
cmd.append(bap_out)
self._run_ff(cmd, vn, "convt")
if "pngquant" in vn.flags:
@@ -1078,19 +1168,21 @@ class ThumbSrv(object):
except:
self.untemp[tpath] = [infile]
bap_in = fsenc(abspath)
bap_out = fsenc(infile)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
b"-map", b"0:a:0",
b"-ac", b"1",
b"-ar", b"48000",
b"-sample_fmt", b"s16",
b"-t", b"900",
b"-y", fsenc(infile),
b"-y", bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "convt")
@@ -1110,20 +1202,22 @@ class ThumbSrv(object):
fc = fc.format(fco)
bap_in = fsenc(infile)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(infile),
b"-i", bap_in,
b"-filter_complex", fc.encode("utf-8"),
b"-map", b"[o]",
b"-frames:v", b"1",
]
# fmt: on
self._ffmpeg_im_o(tpath, vn, cmd)
self._ffmpeg_im_o(bap_out, vn, cmd)
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
quality = self.args.q_mp3.lower()
@@ -1142,24 +1236,26 @@ class ThumbSrv(object):
qk = b"-q:a"
qv = quality[1:].encode("ascii")
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# extremely conservative choices for output format
# (always 2ch 44k1) because if a device is old enough
# to not support opus then it's probably also super picky
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
] + self.big_tags(rawtags) + [
b"-map", b"0:a:0",
b"-ar", b"44100",
b"-ac", b"2",
b"-c:a", b"libmp3lame",
qk, qv,
fsenc(tpath)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1175,16 +1271,18 @@ class ThumbSrv(object):
self.log("conv2 flac", 6)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
b"-map", b"0:a:0",
b"-c:a", b"flac",
fsenc(tpath)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1210,16 +1308,18 @@ class ThumbSrv(object):
self.log("conv2 wav", 6)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
b"-map", b"0:a:0",
b"-c:a", codec,
fsenc(tpath)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1271,19 +1371,21 @@ class ThumbSrv(object):
except:
pass
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
] + tagset + [
b"-map", b"0:a:0",
b"-ac", ac,
] + benc + [
b"-f", container,
fsenc(tpath)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1312,19 +1414,21 @@ class ThumbSrv(object):
self.log("conv2 caf-tmp [%s]" % (enc,), 6)
benc = enc.encode("ascii").split(b" ")
bap_in = fsenc(abspath)
bap_out = fsenc(tmp_opus)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
b"-map_metadata", b"-1",
b"-map", b"0:a:0",
b"-ac", b"2",
] + benc + [
b"-f", b"opus",
fsenc(tmp_opus)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1338,20 +1442,21 @@ class ThumbSrv(object):
if dur < 20 or sz < 256 * 1024:
zs = bq.decode("ascii")
self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6)
bap_in = fsenc(abspath)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", bap_in,
b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix",
b"-map_metadata", b"-1",
b"-ac", b"2",
b"-c:a", b"libopus",
b"-b:a", bq,
b"-f", b"caf",
fsenc(tpath)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1359,18 +1464,19 @@ class ThumbSrv(object):
else:
# simple remux should be safe
self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6)
bap_in = fsenc(tmp_opus)
bap_out = fsenc(tpath)
# fmt: off
cmd = [
HAVE_FFMPEG,
cmd = bwrap(HAVE_FFMPEG[0], bap_in, bap_out) + [
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(tmp_opus),
b"-i", bap_in,
b"-map_metadata", b"-1",
b"-map", b"0:a:0",
b"-c:a", b"copy",
b"-f", b"caf",
fsenc(tpath)
bap_out,
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
@@ -1391,7 +1497,7 @@ class ThumbSrv(object):
return ret
def poke(self, tdir: str) -> None:
if not self.poke_cd.poke(tdir):
if not self.poke_cd or not self.poke_cd.poke(tdir):
return
ts = int(time.time())
+25 -4
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
@@ -22,11 +22,20 @@ from .util import (
quotep,
s3dec,
vjoin,
vjoins,
)
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:
@@ -42,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
@@ -54,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
@@ -81,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:
@@ -322,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()
@@ -437,7 +458,7 @@ class U2idx(object):
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
vp = vjoin(vjoin(vtop, rd), fn)
vp = vjoins(vtop, rd, fn)
if vp in seen_rps:
continue
+294 -111
View File
@@ -1,15 +1,12 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from __future__ import division, print_function, unicode_literals
import errno
import hashlib
import json
import math
import os
import re
import shutil
import stat
import subprocess as sp
import sys
import tempfile
import threading
@@ -19,11 +16,11 @@ from copy import deepcopy
from queue import Queue
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, UNIX, WINDOWS, E
from .authsrv import LEELOO_DALLAS, REDUP_E2, SEESLOG, VFS, AuthSrv
from .bos import bos
from .cfg import vf_bmap, vf_cmap, vf_vmap
from .fsutil import Fstab
from .fsutil import Fstab, filefrag
from .mtag import MParser, MTag
from .util import (
E_FS_CRIT,
@@ -55,6 +52,7 @@ from .util import (
ren_open,
rmdirs,
rmdirs_up,
runcmd,
runhook,
runihook,
s2hms,
@@ -70,9 +68,11 @@ from .util import (
ub64enc,
unhumanize,
vjoin,
vjoins,
vsplit,
w8b64dec,
w8b64enc,
winsparse,
wunlink,
)
@@ -175,6 +175,7 @@ class Up2k(object):
self.pp: Optional[ProgressPrinter] = None
self.rescan_cond = threading.Condition()
self.need_rescan: set[str] = set()
self.have_e2d = False
self.db_act = 0.0
self.reg_mutex = threading.Lock()
@@ -254,6 +255,10 @@ class Up2k(object):
with self.reload_mutex:
if self.reload_flag < n:
self.reload_flag = n
if not self.have_e2d and not self.reloading:
self.reloading = True
Daemon(self._reload_thr)
return
with self.rescan_cond:
self.rescan_cond.notify_all()
@@ -291,7 +296,7 @@ class Up2k(object):
def deferred_init(self) -> None:
all_vols = self.asrv.vfs.all_vols
have_e2d = self.init_indexes(all_vols, [], False)
self.init_indexes(all_vols, [], False)
if self.stop:
# up-mt consistency not guaranteed if init is interrupted;
@@ -309,7 +314,7 @@ class Up2k(object):
return
Daemon(self._snapshot, "up2k-snapshot")
if have_e2d:
if self.have_e2d:
Daemon(self._hasher, "up2k-hasher")
Daemon(self._sched_rescan, "up2k-rescan")
if self.mtag:
@@ -882,17 +887,14 @@ class Up2k(object):
def _expr_idx_filter(self, flags: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
if not self.no_expr_idx:
return False, flags
return False, dict(flags)
ret = {k: v for k, v in flags.items() if not k.startswith("e2t")}
if len(ret) == len(flags):
return False, flags
return True, ret
return len(ret) != len(flags), flags
def init_indexes(
self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0
) -> bool:
) -> None:
if not gid:
with self.mutex:
gid = self.gid
@@ -907,7 +909,7 @@ class Up2k(object):
with self.mutex:
if gid != self.gid:
return False
return
if self.pp:
continue
@@ -946,6 +948,9 @@ class Up2k(object):
with self.mutex, self.reg_mutex:
# only need to protect register_vpath but all in one go feels right
for vol in vols:
if vol.realpath:
self.flags[vol.realpath] = dict(vol.flags)
if bos.path.isfile(vol.realpath):
self.volstate[vol.vpath] = "online (just-a-file)"
t = "NOTE: volume [/%s] is a file, not a folder"
@@ -995,6 +1000,21 @@ class Up2k(object):
with self.mutex, self.reg_mutex:
self._drop_caches()
def setstate(vol, n):
t = ""
if n < 1 and "e2v" in vol.flags:
t += "+e2v"
if n < 2 and vol.flags.get("redup"):
t += "+redup"
if n < 3 and "e2ts" in vol.flags:
t += "+tags"
if t:
t = "online (%s pending)" % (t[1:],)
else:
t = "online, idle"
self.volstate[vol.vpath] = t
for vol in vols:
if self.stop or gid != self.gid:
break
@@ -1011,26 +1031,16 @@ class Up2k(object):
if vac:
need_vac[vol] = True
if "e2v" in vol.flags:
t = "online (integrity-check pending)"
elif "e2ts" in vol.flags:
t = "online (tags pending)"
else:
t = "online, idle"
self.volstate[vol.vpath] = t
setstate(vol, 0)
self._unblock()
# file contents verification
for vol in vols:
if self.stop:
break
if "e2v" not in vol.flags:
if self.stop or "e2v" not in vol.flags:
continue
t = "online (verifying integrity)"
t = "online (doing e2v)"
self.volstate[vol.vpath] = t
self.log("{} [{}]".format(t, vol.realpath))
@@ -1039,12 +1049,23 @@ class Up2k(object):
self.log("modified {} entries in the db".format(nmod), 3)
need_vac[vol] = True
if "e2ts" in vol.flags:
t = "online (tags pending)"
else:
t = "online, idle"
setstate(vol, 1)
# convert dedup type
for vol in vols:
if self.stop or not vol.flags.get("redup"):
continue
t = "online (doing redup)"
self.volstate[vol.vpath] = t
self.log("{} [{}]".format(t, vol.realpath))
try:
with self.mutex, self.reg_mutex:
self._redup(vol, vols)
except:
self.log("redup failed: " + min_ex(), 1)
setstate(vol, 2)
# open the rest + do any e2ts(a)
needed_mutagen = False
@@ -1079,7 +1100,7 @@ class Up2k(object):
cur.execute("vacuum")
if self.stop:
return False
return
for vol in all_vols.values():
if vol.flags["dbd"] == "acid":
@@ -1109,7 +1130,7 @@ class Up2k(object):
self.log("checkpoint failed: {}".format(ex), 3)
if self.stop:
return False
return
self.pp.end = True
@@ -1129,7 +1150,7 @@ class Up2k(object):
else:
self.unpp()
return have_e2d
self.have_e2d = self.have_e2d or have_e2d
def register_vpath(
self, ptop: str, flags: dict[str, Any]
@@ -1153,12 +1174,12 @@ class Up2k(object):
vpath = k
_, flags = self._expr_idx_filter(flags)
n4g = bool(flags.get("noforget"))
n4g = "noforget" in flags
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 xvd zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@@ -1292,6 +1313,17 @@ class Up2k(object):
self.log(t % (vpath, ex), 1)
return None
if not (
ANYWIN or UNIX or "hist_cow" in flags or "up2k.db" in os.listdir(histpath)
):
try:
if self.fstab.get(histpath)[0] != "btrfs":
raise Exception()
runcmd([b"chattr", b"-c", fsenc(histpath)], 1) # compression and...
runcmd([b"chattr", b"+C", fsenc(histpath)], 1) # no-cow are incompat
except:
pass
try:
cur = self._open_db_wd(db_path)
@@ -1375,14 +1407,15 @@ class Up2k(object):
top = vol.realpath
rei = vol.flags.get("noidx")
reh = vol.flags.get("nohash")
n4g = bool(vol.flags.get("noforget"))
n4g = "noforget" in vol.flags
ffat = "fat32" in vol.flags
cst = bos.stat(top)
dev = cst.st_dev if vol.flags.get("xdev") else 0
dev = cst.st_dev if "xdev" in vol.flags else 0
with self.mutex:
with self.reg_mutex:
reg = self.register_vpath(top, vol.flags)
self.flags[vol.realpath] = dict(vol.flags)
assert reg and self.pp # !rm
cur, db_path = reg
@@ -1430,12 +1463,14 @@ class Up2k(object):
rtop,
rei,
reh,
vol.flags["th_coversd"],
vol.flags["th_coversd_set"],
n4g,
ffat,
[],
cst,
dev,
bool(vol.flags.get("xvol")),
"xvol" in vol.flags,
)
if not n4g:
n_rm = self._drop_lost(db.c, top, excl)
@@ -1507,6 +1542,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],
@@ -1532,8 +1569,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
@@ -1600,6 +1635,8 @@ class Up2k(object):
rap,
rei,
reh,
th_cvd,
th_cvds,
n4g,
fat32,
seen,
@@ -1630,7 +1667,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 (
@@ -2071,6 +2108,138 @@ class Up2k(object):
return len(rewark) + len(f404)
def _redup(self, vol: VFS, vols: list[VFS]) -> None:
logmsg = "redup: replacing %r with %r"
dry = "redup_dry" in vol.flags
if dry:
logmsg += " #dry"
zs, to = vol.flags["redup"].split("=")
zsl = zs.split(",")
cref = "no" in zsl or "ref" in zsl
csym = "sym" in zsl
chard = "hard" in zsl
chard0 = chard
vf = vol.flags.copy()
vf["dedup"] = 1
for zs in "hardlink hardlinkonly reflink".split():
vf.pop(zs, None)
if to == "ref":
vf["reflink"] = 1
elif to == "hard":
vf["hardlinkonly"] = vf["hardlink"] = 1
elif to == "sym":
pass
else:
raise Exception("target type not implemented: " + to)
if (csym or chard) and to != "sym":
self.log("redup: stage 1 begin")
if to == "hard":
chard = False
for x in vol.walk("", "", [], LEELOO_DALLAS, [[]], 2, True, True, True):
vn, _, _, atop, lsf, lsd, lsv = x
if vn is not vol:
lsd[:] = []
lsv.clear()
continue
for fn, st in lsf:
if (csym and stat.S_ISLNK(st.st_mode)) or (
chard and st.st_nlink > 1
):
ap = os.path.join(atop, fn)
self.log(logmsg % (ap, absreal(ap)))
if dry:
continue
ap2 = tempfile.NamedTemporaryFile(
prefix="r,", dir=atop, delete=False
).name
self._symlink(ap, ap2, vf, True, True, st.st_mtime)
st1 = bos.stat(ap)
st2 = bos.lstat(ap2)
if st1.st_size != st2.st_size or stat.S_ISLNK(st2.st_mode):
wunlink(self.log, ap2, vf)
raise Exception("redup: clone failed; giving up")
wunlink(self.log, ap, vf)
bos.rename(ap2, ap)
if cref or to == "sym":
self.log("redup: stage 2 begin")
if "e2ds" not in vol.flags:
self.log(REDUP_E2 % (vol.vpath,), 3)
cur = self.cur[vol.realpath]
curs = [(vol, cur.connection.cursor())]
if to != "sym":
for v2 in vols:
if vol is not v2 and v2.realpath in self.cur:
curs += [(v2, self.cur[v2.realpath])]
nrem = cur.execute("select count(w) from up").fetchone()[0]
self.log("redup [/%s]: %d files left" % (vol.vpath, nrem))
q2 = "select rd, fn from up where substr(w,1,16) = ? and w=?"
for w, rd, fn in cur.execute("select w, rd, fn from up"):
w16 = w[:16]
for v2, c2 in curs:
hits = c2.execute(q2, (w16, w)).fetchall()
if not hits:
continue
rd2, fn2 = hits[0]
if fn == fn2 and rd == rd2 and vol is v2:
continue
apt = ""
try:
rd, fn = s3dec(rd, fn)
rd2, fn2 = s3dec(rd2, fn2)
fp1 = os.path.join(vol.realpath, rd, fn)
fp2 = os.path.join(v2.realpath, rd2, fn2)
st1 = bos.lstat(fp1)
if fp1 == fp2 or stat.S_ISLNK(st1.st_mode):
continue # self(?), or redup failed during walk
if to == "hard" and st1.st_nlink >= len(hits):
continue
ap1 = absreal(fp1)
ap2 = absreal(fp2)
st2 = bos.lstat(ap2)
if stat.S_ISLNK(st2.st_mode):
continue # dead hit
if to == "ref" and ap1 != ap2:
try:
frag1 = filefrag(ap1)
if frag1:
frag2 = filefrag(ap2)
if frag1 == frag2:
continue
except Exception as ex:
self.log("redup: filefrag failed (OK); %r" % (ex,))
for ap in (ap1,) if ap1 == ap2 else (ap1, ap2):
self.log("redup: integrity-checking %r" % (ap,))
zsl, st = self._hashlist_from_file(ap)
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
if w2 != w:
t = "db desync:\n%s %r\n%s db"
raise Exception(t % (w2, ap, w))
self.log(logmsg % (ap1, ap2))
if dry:
continue
apt = tempfile.NamedTemporaryFile(
prefix="r,", dir=os.path.dirname(fp1), delete=False
).name
self._symlink(ap2, apt, vf, True, True, st1.st_mtime)
zsl, st = self._hashlist_from_file(apt)
w2 = up2k_wark_from_hashlist(self.salt, st.st_size, zsl)
if w != w2:
raise Exception("bad checksum after copy?!")
wunlink(self.log, ap1, vf)
bos.rename(apt, ap1)
break
except Exception as ex:
self.log("redup: skipping match due to " + str(ex))
if apt and os.path.exists(apt):
os.unlink(apt)
continue
def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]:
ptop = vol.realpath
with self.mutex, self.reg_mutex:
@@ -2730,6 +2899,17 @@ class Up2k(object):
t = "-" * 72
raise Exception("%s\n%s\n%s" % (t, txt, t))
def _sql_ok(self, cur: "sqlite3.Cursor", q: str) -> bool:
try:
cur.execute(q).fetchone()
return True
except Exception as ex:
if "no such table" not in str(ex):
t = "WARNING: database issues, maybe due to server filesystem or hardware (check %sOS-logs):\n %r -- \033[31m%r\n"
t2 = "" if ANYWIN or UNIX else "dmesg / "
self.log(t % (t2, q, ex), 3)
return False
def _orz(self, db_path: str) -> "sqlite3.Cursor":
assert sqlite3 # type: ignore # !rm
c = sqlite3.connect(
@@ -2903,11 +3083,8 @@ class Up2k(object):
def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None:
# v5 -> v5a
try:
cur.execute("select d, h from dh limit 1").fetchone()
if self._sql_ok(cur, "select d, h from dh limit 1"):
return
except:
pass
for cmd in [
r"create table dh (d text, h text)",
@@ -2921,11 +3098,8 @@ class Up2k(object):
def _add_xiu_tab(self, cur: "sqlite3.Cursor") -> None:
# v5a -> v5b
# store rd+fn rather than warks to support nohash vols
try:
cur.execute("select c, w, rd, fn from iu limit 1").fetchone()
if self._sql_ok(cur, "select c, w, rd, fn from iu limit 1"):
return
except:
pass
try:
cur.execute("drop table iu")
@@ -2943,11 +3117,8 @@ class Up2k(object):
def _add_cv_tab(self, cur: "sqlite3.Cursor") -> None:
# v5b -> v5c
try:
cur.execute("select rd, dn, fn from cv limit 1").fetchone()
if self._sql_ok(cur, "select rd, dn, fn from cv limit 1"):
return
except:
pass
for cmd in [
r"create table cv (rd text, dn text, fn text)",
@@ -2982,11 +3153,8 @@ class Up2k(object):
def _add_ds_tab(self, cur: "sqlite3.Cursor") -> None:
# v5d -> v5e
try:
cur.execute("select rd, sz from ds limit 1").fetchone()
if self._sql_ok(cur, "select rd, sz from ds limit 1"):
return
except:
pass
for cmd in [
r"create table ds (rd text, sz int, nf int)",
@@ -3037,7 +3205,7 @@ class Up2k(object):
raise Pebkac(500, "too many xbu relocs, giving up")
ptop = cj["ptop"]
if not self.register_vpath(ptop, cj["vcfg"]):
if not self.register_vpath(ptop, cj.pop("vcfg")):
if ptop not in self.registry:
raise Pebkac(410, "location unavailable")
@@ -3059,17 +3227,20 @@ class Up2k(object):
jcur = self.cur.get(ptop)
reg = self.registry[ptop]
vfs = self.vfs.all_vols[cj["vtop"]]
n4g = bool(vfs.flags.get("noforget"))
noclone = bool(vfs.flags.get("noclone"))
rand = vfs.flags.get("rand") or cj.get("rand")
n4g = "noforget" in vfs.flags
noclone = "noclone" in vfs.flags
rand = "rand" in vfs.flags or cj.get("rand")
lost: list[tuple["sqlite3.Cursor", str, str]] = []
safe_dedup = vfs.flags.get("safededup") or 50
data_ok = safe_dedup < 10 or n4g
vols = [(ptop, jcur)] if jcur else []
if vfs.flags.get("xlink"):
if vols and ("xlink" in vfs.flags or "reflink" in vfs.flags):
vols += [(k, v) for k, v in self.cur.items() if k != ptop]
if "xlink" not in vfs.flags:
zs = self.fstab.get(ptop)[1]
vols = [x for x in vols if self.fstab.get(x[0])[1] == zs]
if noclone:
wark = up2k_wark_from_metadata(
@@ -3190,7 +3361,7 @@ class Up2k(object):
c2 = None
for cur, dp_dir, dp_fn in lost:
t = "forgetting desynced db entry: %r"
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
self.log(t % ("/" + vjoins(vfs.vpath, dp_dir, dp_fn)))
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
if c2 and c2 != cur:
c2.connection.commit()
@@ -3280,7 +3451,10 @@ class Up2k(object):
vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"])
if "done" not in job:
if "rvp0" in job and wark in reg:
# xbu reloc; accept wrong path
job["addr"] = cj["addr"]
elif "done" not in job:
self.log("unfinished:\n %r\n %r" % (src, dst))
err = "partial upload exists at a different location; please resume uploading here instead:\n"
err += "/" + quotep(vsrc) + " "
@@ -3370,18 +3544,29 @@ class Up2k(object):
x = pathmod(self.vfs, dst, vp, hr["reloc"])
if x:
ud1 = (vfs.vpath, job["prel"], job["name"])
job["rvp0"] = vjoins(*ud1)
pdir, _, job["name"], (vfs, rem) = x
dst = os.path.join(pdir, job["name"])
job["vcfg"] = vfs.flags
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"])
job["name"] = vfs.sanitize_fn(job["name"], rem)
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4))
job["vcfg"] = vfs.flags
job["hash"] = cj["hash"]
self.log("xbu reloc1:%d..." % (depth,), 6)
t = "xbu reloc1=%d ptop=%r vtop=%r prel=%r name=%r"
t = t % (
depth,
job["ptop"],
job["vtop"],
job["prel"],
job["name"],
)
self.log(t, 6)
zs = djoin(job["ptop"], job["prel"])
bos.makedirs(zs, vf=vfs.flags)
return self._handle_json(job, depth + 1)
job["name"] = self._untaken(pdir, job, now)
@@ -3545,7 +3730,7 @@ class Up2k(object):
st = bos.stat(fp)
try:
vrel = vjoin(job["prel"], fname)
xlink = bool(vf.get("xlink"))
xlink = "xlink" in vf
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
except Exception as ex:
@@ -3600,7 +3785,7 @@ class Up2k(object):
if rm and bos.path.exists(dst):
wunlink(self.log, dst, flags)
if not is_mv and not flags.get("dedup"):
if not is_mv and "dedup" not in flags:
raise Exception("dedup is disabled in config")
if "reflink" in flags:
@@ -4103,31 +4288,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)"
@@ -4141,6 +4302,9 @@ class Up2k(object):
pass
def handle_fs_abrt(self, akey: str) -> None:
if akey == "ping":
with self.mutex, self.reg_mutex:
return
self.abrt_key = akey
def handle_rm(
@@ -4245,7 +4409,7 @@ class Up2k(object):
is_dir = stat.S_ISDIR(st.st_mode)
except:
# NOTE: "file not found" *sftpd
raise Pebkac(400, "file not found on disk (already deleted?)")
raise Pebkac(404, "file not found on disk (already deleted?)")
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
raise Pebkac(400, "file does not exist case-sensitively")
@@ -4322,7 +4486,7 @@ class Up2k(object):
cur = None
try:
ptop = dbv.realpath
xlink = bool(dbv.flags.get("xlink"))
xlink = "xlink" in dbv.flags
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file(
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
@@ -4526,6 +4690,12 @@ class Up2k(object):
self.log("not found in src db: %r" % (svp,))
try:
if svn_dbv != dvn and not (
"xlink" in dvn.flags
or "reflink" in dvn.flags
or "hardlinkonly" in dvn.flags
):
raise OSError(errno.EXDEV, "different volumes")
if is_link and st != stl:
# relink non-broken symlinks to still work after the move,
# but only resolve 1st level to maintain relativity
@@ -4740,7 +4910,7 @@ class Up2k(object):
fsize,
ip,
time.time(),
None,
[dabs],
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
@@ -4783,7 +4953,7 @@ class Up2k(object):
fsize,
ip,
time.time(),
None,
[sabs],
)
return "k"
@@ -4803,7 +4973,7 @@ class Up2k(object):
raise Pebkac(400, t)
self._copy_tags(c1, c2, w)
xlink = bool(svn.flags.get("xlink"))
xlink = "xlink" in svn.flags
with self.reg_mutex:
has_dupes = self._forget_file(
@@ -4913,7 +5083,7 @@ class Up2k(object):
fsize,
ip,
time.time(),
None,
[sabs],
)
return "k"
@@ -5217,7 +5387,7 @@ class Up2k(object):
if eno not in E_FS_CRIT:
return self.log("hashing failed; %r @ %r\n%s" % (ex, ap, min_ex()), 3)
t = "hashing failed; %r @ %r\n%s\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\n"
t2 = "" if ANYWIN or MACOS else "dmesg and "
t2 = "" if ANYWIN or UNIX else "dmesg and "
return self.log(t % (ex, ap, min_ex(), t2), 1)
def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]:
@@ -5263,15 +5433,25 @@ class Up2k(object):
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
if x:
ud1 = (vfs.vpath, job["prel"], job["name"])
job["rvp0"] = vjoins(*ud1)
pdir, _, job["name"], (vfs, rem) = x
job["vcfg"] = vf = vfs.flags
vf = vfs.flags
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"])
job["name"] = vfs.sanitize_fn(job["name"], rem)
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
self.log("xbu reloc2:%d..." % (depth,), 6)
job["vcfg"] = vf
t = "xbu reloc2=%d ptop=%r vtop=%r prel=%r name=%r" % (
depth,
job["ptop"],
job["vtop"],
job["prel"],
job["name"],
)
self.log(t, 6)
bos.makedirs(djoin(job["ptop"], job["prel"]), vf=vf)
return self._handle_json(job, depth + 1)
job["name"] = self._untaken(pdir, job, job["t0"])
@@ -5311,9 +5491,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
@@ -5700,7 +5883,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
+189 -45
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
@@ -10,7 +10,6 @@ import hashlib
import hmac
import json
import logging
import math
import mimetypes
import os
import platform
@@ -180,12 +179,16 @@ try:
raise ImportError()
from .stolen.ifaddr import get_adapters
IFADDR_VND = True
except ImportError:
IFADDR_VND = False
from ifaddr import get_adapters
HAVE_IFADDR = True
except:
HAVE_IFADDR = False
IFADDR_VND = False
def get_adapters(include_unconfigured=False):
return []
@@ -195,10 +198,10 @@ try:
if os.environ.get("PRTY_NO_SQLITE"):
raise Exception()
HAVE_SQLITE3 = True
import sqlite3
assert hasattr(sqlite3, "connect") # graalpy
HAVE_SQLITE3 = True
except:
HAVE_SQLITE3 = False
@@ -213,8 +216,9 @@ try:
if os.environ.get("PRTY_NO_PSUTIL"):
raise Exception()
HAVE_PSUTIL = True
import psutil
HAVE_PSUTIL = True
except:
HAVE_PSUTIL = False
@@ -225,8 +229,10 @@ try:
raise Exception()
import magic
HAVE_MAGIC = True
except:
pass
HAVE_MAGIC = False
if os.environ.get("PRTY_MODSPEC"):
from inspect import getsourcefile
@@ -283,7 +289,6 @@ try:
raise Exception()
socket.inet_pton(socket.AF_INET6, "::1")
HAVE_IPV6 = True
if GRAAL:
try:
@@ -298,13 +303,15 @@ try:
return _inet_pton(fam, ip)
socket.inet_pton = inet_pton
HAVE_IPV6 = True
except:
HAVE_IPV6 = False
def inet_pton(fam, ip):
return socket.inet_aton(ip)
socket.inet_pton = inet_pton
HAVE_IPV6 = False
try:
@@ -326,7 +333,14 @@ except:
BITNESS = struct.calcsize("P") * 8
CAN_SIGMASK = not (ANYWIN or PY2 or GRAAL)
try:
if ANYWIN or PY2 or GRAAL or not hasattr(signal, "pthread_sigmask"):
raise Exception()
BLOCK_SIGS = [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGUSR1]
CAN_SIGMASK = True
except:
BLOCK_SIGS = []
CAN_SIGMASK = False
RE_ANSI = re.compile("\033\\[[^mK]*[mK]")
@@ -345,19 +359,25 @@ RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
if PY2:
def umktrans(s1, s2):
def umktrans(s1: str, s2: str):
return {ord(c1): ord(c2) for c1, c2 in zip(s1, s2)}
else:
umktrans = str.maketrans
FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/")
VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\")
APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/")
FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":")
FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None
VPTL_MAC = umktrans(":", ":")
VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None
APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None
BADNAMES = []
if ANYWIN:
BADNAMES += ["con", "prn", "aux", "nul"]
BADNAMES += ["com%s" % (n,) for n in range(1, 10)]
BADNAMES += ["lpt%s" % (n,) for n in range(1, 10)]
BADNAMES += ["com%s" % (n,) for n in ("¹", "²", "³")]
BADNAMES += ["lpt%s" % (n,) for n in ("¹", "²", "³")]
BADNAMESET = set(BADNAMES)
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@@ -422,6 +442,7 @@ IMPLICATIONS = [
["e2vu", "e2v"],
["e2vp", "e2v"],
["e2v", "e2d"],
["srch_nfkc", "srch_icase"],
["hardlink_only", "hardlink"],
["hardlink", "dedup"],
["tftpvv", "tftpv"],
@@ -429,12 +450,27 @@ IMPLICATIONS = [
["no_dupe_m", "no_dupe"],
["no_html", "no_script"],
["nohtml", "noscript"],
["no_html", "nohtml"], # user-typo
["no_script", "noscript"], # user-typo
["no_readmes", "no_readme"], # user-typo
["noreadmes", "no_readme"], # user-typo
["noreadme", "no_readme"], # user-typo
["no_logue", "no_logues"], # user-typo
["nologues", "no_logues"], # user-typo
["nologue", "no_logues"], # user-typo
["nw", "redup_dry"],
["plainlogue", "plainlogues"], # user-typo
["plainreadmes", "plainreadme"], # user-typo
["noscript", "plainlogues"],
["noscript", "plainreadme"],
["sftpvv", "sftpv"],
["smbw", "smb"],
["smb1", "smb"],
["smbvvv", "smbvv"],
["smbvv", "smbv"],
["smbv", "smb"],
["xdev", "xvd"],
["xvol", "xvd"],
["zv", "zmv"],
["zv", "zsv"],
["z", "zm"],
@@ -650,6 +686,20 @@ if EXE:
pass
try:
if PY2 or ANYWIN:
raise Exception()
HAVE_BWRAP = shutil.which("bwrap")
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])
@@ -832,10 +882,8 @@ class Daemon(threading.Thread):
self.start()
def run(self):
if CAN_SIGMASK:
signal.pthread_sigmask(
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
)
if BLOCK_SIGS:
signal.pthread_sigmask(signal.SIG_BLOCK, BLOCK_SIGS)
self.fun(*self.a, **self.ka)
@@ -1329,7 +1377,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)
@@ -1778,9 +1826,7 @@ def log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> No
def _sigblock():
signal.pthread_sigmask(
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
)
signal.pthread_sigmask(signal.SIG_BLOCK, BLOCK_SIGS)
sigblock = _sigblock if CAN_SIGMASK else noop
@@ -2120,7 +2166,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
@@ -2186,7 +2233,7 @@ def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
raise Pebkac(
400,
"protocol error while reading headers",
log=ret.decode("utf-8", "replace"),
log=repr(ret.decode("utf-8", "replace")),
)
ofs = ret.find(b"\r\n\r\n")
@@ -2390,7 +2437,7 @@ def uncyg(path: str) -> str:
return "%s:\\%s" % (path[1], path[3:])
def undot(path: str) -> str:
def _undot(path: str) -> str:
ret: list[str] = []
for node in path.split("/"):
if node == "." or not node:
@@ -2406,30 +2453,56 @@ def undot(path: str) -> str:
return "/".join(ret)
def sanitize_fn(fn: str) -> str:
fn = fn.replace("\\", "/").split("/")[-1]
if APTL_OS:
fn = sanitize_to(fn, APTL_OS)
def _undot_win(path: str) -> str:
ret: list[str] = []
for node in path.replace("\\", "/").split("/"):
if node == "..":
if ret:
ret.pop()
continue
node = node.rstrip(". ")
if node:
ret.append(node)
return "/".join(ret)
if ANYWIN:
undot = _undot_win
else:
undot = _undot
def sanitize_fn(fn: str, fsnt: Optional[str] = None) -> str:
fn = fn.replace("\x00", "_").split("/")[-1]
if fsnt and fsnt != "lin":
fn = fn.translate(VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC)
fn2 = fn.replace("\\", "/")
if ANYWIN or fsnt == "win" or "/../" in fn2:
fn = fn2.split("/")[-1].rstrip(". ")
if VPTL_OS:
fn = sanitize_to(fn, VPTL_OS)
return fn.strip()
def sanitize_to(fn: str, tl: dict[int, int]) -> str:
def sanitize_to(fn: str, tl: dict[int, int], fsnt: Optional[str] = None) -> str:
fn = fn.translate(tl)
if ANYWIN:
bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10):
bad += ("com%s lpt%s" % (n, n)).split(" ")
if fn.lower().split(".")[0] in bad:
if ANYWIN or fsnt == "win":
fn = fn.rstrip(". ")
if fn.lower().split(".")[0].strip() in BADNAMESET:
fn = "_" + fn
return fn
def sanitize_vpath(vp: str) -> str:
if not APTL_OS:
def sanitize_vpath(vp: str, fsnt: Optional[str] = None) -> str:
vptl = VPTL_OS
if fsnt and fsnt != "lin":
vptl = VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC
if not vptl:
return vp
parts = vp.replace(os.sep, "/").split("/")
ret = [sanitize_to(x, APTL_OS) for x in parts]
ret = [sanitize_to(x, vptl, fsnt) for x in parts]
return "/".join(ret)
@@ -2676,6 +2749,10 @@ def vjoin(rd: str, fn: str) -> str:
return rd or fn
def vjoins(*a: str) -> str:
return "/".join([x for x in a if x])
# url-join
def ujoin(rd: str, fn: str) -> str:
if rd and fn:
@@ -2920,6 +2997,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:
@@ -2971,7 +3067,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
@@ -3008,7 +3106,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:
@@ -3988,9 +4086,9 @@ def runihook(
}
for x, vp, ap in zip(ups, vps, aps)
]
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace")
sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace") + b"\n"
else:
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps)
sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps) + b"\n"
if acmd[0].startswith("zmq:"):
try:
@@ -4005,7 +4103,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:
@@ -4171,6 +4269,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:
@@ -4179,7 +4281,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)
@@ -4334,6 +4436,36 @@ def gzip_file_orig_sz(f) -> int:
return sunpack(b"I", rv)[0] # type: ignore
def zip_fi(zf, fp, max_sz):
zi = zf.getinfo(fp)
if max_sz and zi.file_size > max_sz:
raise Pebkac(404, "zip bomb defused")
return zi
def zip_lim(zi, max_sz):
if zi.file_size > max_sz:
raise Pebkac(404, "zip bomb defused")
return zi
def zip_read(zf, zi, max_sz):
ret = b""
with zf.open(zi) as f:
while True:
buf = f.read(max_sz)
if not buf:
break
ret += buf
if len(ret) >= max_sz:
raise Pebkac(404, "zip bomb defused")
return ret
def zip_readf(zf, zp, max_sz):
return zip_read(zf, zip_fi(zf, zp, max_sz), max_sz)
def align_tab(lines: list[str]) -> list[str]:
rows = []
ncols = 0
@@ -4474,6 +4606,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 = {}
+2 -1
View File
@@ -7,7 +7,8 @@ pk: $(addsuffix .gz, $(wildcard tl/*.js *.js *.css) \
un: $(addsuffix .un, $(wildcard tl/*.gz *.gz a/*.gz))
tl/%.js.gz: tl/%.js
./Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
sh Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@
pigz -dc <$@ | grep -q ^Ls
touch -r $< $@
rm $<
+49 -6
View File
@@ -28,7 +28,7 @@ window.baguetteBox = (function () {
onChange: null,
readDirRtl: false,
},
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnZoom, btnVmode, btnReadDir, btnClose,
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnZoom, btnVmode, btnReadDir, btnGoPage, btnClose,
currentGallery = [],
currentIndex = 0,
isOverlayVisible = false,
@@ -46,6 +46,7 @@ window.baguetteBox = (function () {
imagesElements = [],
documentLastFocus = null,
isFullscreen = false,
isCbz = false,
vmute = false,
vloop = sread('vmode') == 'L',
vnext = sread('vmode') == 'C',
@@ -137,6 +138,9 @@ window.baguetteBox = (function () {
};
var trapFocusInsideOverlay = function (e) {
if (modal.busy)
return;
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {
e.stopPropagation();
btnClose.focus();
@@ -196,6 +200,16 @@ window.baguetteBox = (function () {
return [selectorData.galleries, options];
}
function getCbzPage() {
try {
var ret = parseInt(/,p([0-9]+)(,|$)/.exec(location.hash)[1]);
if (isNum(ret))
return Math.max(Math.min(ret, currentGallery.length), 1) - 1;
}
catch (ex) { }
return 0;
}
function bindCbzClickListeners(tagsNodeList, userOptions) {
var cbzNodes = [].filter.call(tagsNodeList, function (element) {
return re_cbz.test(element.href);
@@ -212,10 +226,9 @@ window.baguetteBox = (function () {
e.preventDefault ? e.preventDefault() : e.returnValue = false;
fillCbzGallery(gallery, cbzElement, eventHandler).then(function () {
prepareOverlay(gallery, userOptions);
showOverlay(0);
}
).catch(function (reason) {
prepareOverlay(gallery, userOptions, true);
showOverlay(getCbzPage());
}).catch(function (reason) {
console.error("cbz-ded", reason);
var t;
try {
@@ -269,6 +282,7 @@ window.baguetteBox = (function () {
href: imageHref,
imageElement: cbzElement,
eventHandler: eventHandler,
page: index + 1,
};
gallery.push(galleryItem);
});
@@ -317,6 +331,7 @@ window.baguetteBox = (function () {
'<button id="bbox-full" type="button" tt="full-screen">⛶</button>' +
'<button id="bbzoom" type="button" tt="zoom/stretch">z</button>' +
'<button id="bbox-vmode" type="button" tt="a"></button>' +
'<button id="bbox-gopage" type="button" tt="go to page">pg</button>' +
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
'</div></div>'
);
@@ -336,6 +351,7 @@ window.baguetteBox = (function () {
btnFull = ebi('bbox-full');
btnZoom = ebi('bbzoom');
btnVmode = ebi('bbox-vmode');
btnGoPage = ebi('bbox-gopage');
btnClose = ebi('bbox-close');
bcfg_bind(options, 'bbzoom', 'bbzoom', false, setzoom);
@@ -357,6 +373,7 @@ window.baguetteBox = (function () {
['F', 'toggle fullscreen'],
['Z', 'toggle zoom/stretch'],
['S', 'toggle file selection'],
['G', 'cbz: go to page'],
['space, P, K', 'video: play / pause'],
['U', 'video: seek 10sec back'],
['O', 'video: seek 10sec ahead'],
@@ -442,6 +459,8 @@ window.baguetteBox = (function () {
btnZoom.click();
else if (kl == "s")
tglsel();
else if (kl == "g" && isCbz)
gotoCbzPage(e);
else if (kl == "r")
rotn(e.shiftKey ? -1 : 1);
else if (kl == "y")
@@ -507,6 +526,24 @@ window.baguetteBox = (function () {
v.play();
}
function gotoCbzPage(e) {
ev(e);
var cur = currentIndex + 1,
max = currentGallery.length;
modal.prompt('Go to page (1-' + max + ')', '' + cur, function (v) {
if (!v)
return;
v = parseInt(v);
if (!isNum(v))
return toast.warn(4, 'invalid input');
show(Math.max(Math.min(v, max), 1) - 1);
});
}
function tglVmode() {
if (vloop) {
vnext = true;
@@ -636,6 +673,7 @@ window.baguetteBox = (function () {
bind(btnRotR, 'click', rotr);
bind(btnSel, 'click', tglsel);
bind(btnFull, 'click', tglfull);
bind(btnGoPage, 'click', gotoCbzPage);
bind(slider, 'contextmenu', contextmenuHandler);
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
@@ -660,18 +698,23 @@ window.baguetteBox = (function () {
unbind(btnRotR, 'click', rotr);
unbind(btnSel, 'click', tglsel);
unbind(btnFull, 'click', tglfull);
unbind(btnGoPage, 'click', gotoCbzPage);
unbind(slider, 'contextmenu', contextmenuHandler);
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
timer.rm(rotn);
clearTimeout(unrot_timer);
}
function prepareOverlay(gallery, userOptions) {
function prepareOverlay(gallery, userOptions, cbz) {
if (currentGallery === gallery)
return;
isCbz = cbz;
btnGoPage.style.display = isCbz ? '' : 'none';
currentGallery = gallery;
setOptions(userOptions);
slider.innerHTML = '';
+4
View File
@@ -1768,6 +1768,10 @@ html.y #tree.nowrap .ntree a+a:hover {
top: .3em;
z-index: 9;
}
.dumb_loader_thing.ns {
animation: none;
opacity: 1;
}
#dlt_t {
left: 0;
}
+11 -8
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>
@@ -131,8 +133,9 @@
<div id="rcm" tabindex="0"></div>
<script>
<script nonce="{{ js_nonce }}">
var SR = "{{ r }}",
JS_NONCE = "{{ js_nonce }}",
CGV1 = {{ cgv1 }},
CGV = {{ cgv|tojson }},
TS = "{{ ts }}",
@@ -146,17 +149,17 @@
var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || dtheme;
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
{%- if lang != "eng" %}
<script src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
{%- endif %}
<script src="{{ r }}/.cpr/w/baguettebox.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/browser.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/up2k.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/baguettebox.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/browser.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/up2k.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_BBX","baguettebox");
+128 -52
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"],
@@ -579,9 +580,10 @@ if (1)
"u_https1": "you should",
"u_https2": "switch to https",
"u_https3": "for better performance",
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" id="u2nah">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+",
"b2old": 'your browser is too old',
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",
@@ -665,6 +667,7 @@ if (1)
"rc_pla": "play as audio",
"rc_txt": "open in textfile viewer",
"rc_md": "open in markdown viewer",
"rc_wopi": "open in office editor",
"rc_dl": "download",
"rc_zip": "download as archive",
"rc_cpl": "copy link",
@@ -757,7 +760,7 @@ if (window.glang && navigator.languages && !/\bcplng=/.test(document.cookie))
if (window.stop)
window.stop();
document.body.innerHTML = 'Loading ' + n;
setck("cplng=" + n, location.reload.bind(location));
setck("cplng=" + n, relod);
crashed = true;
throw 1;
}
@@ -865,7 +868,7 @@ ebi('widget').innerHTML = (
// up2k ui
ebi('op_up2k').innerHTML = (
'<form id="u2form" method="post" enctype="multipart/form-data" onsubmit="return false;"></form>\n' +
'<form id="u2form" method="post" enctype="multipart/form-data"></form>\n' +
'<table id="u2conf">\n' +
' <tr>\n' +
@@ -944,6 +947,9 @@ ebi('op_up2k').innerHTML = (
'<div id="u2life"></div>' +
'<div id="u2foot"></div>'
);
ebi('u2form').onsubmit = function () {
return false;
};
ebi('wrap').insertBefore(mknod('div', 'lazy'), ebi('epi'));
@@ -1108,6 +1114,7 @@ ebi('rcm').innerHTML = (
'<a href="#" id="rpla">' + L.rc_pla + '</a>' +
'<a href="#" id="rtxt">' + L.rc_txt + '</a>' +
'<a href="#" id="rmd">' + L.rc_md + '</a>' +
'<a href="#" id="rwopi">' + L.rc_wopi + '</a>' +
'<div id="rs1" class="sep"></div>' +
'<a href="#" id="rcpl">' + L.rc_cpl + '</a>' +
'<a href="#" id="rdl">' + L.rc_dl + '</a>' +
@@ -1198,6 +1205,8 @@ function goto(dest) {
if (treectl)
treectl.onscroll();
}
function go2bup() { goto('bup'); }
function go2up2k() { goto('up2k'); }
var m = SPINNER.split(','),
@@ -1240,8 +1249,11 @@ check_image_support('jxl', "data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA=");
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;
/\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|jxl|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i :
/\.(a?png|avif|bmp|gif|jpe?g|jfif|jxl|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) {
@@ -1783,7 +1795,7 @@ function MPlayer() {
continue;
tid = tid.slice(1);
if (r.tracks[tid])
if (r.tracks[tid])
order.push(tid);
}
r.order = order;
@@ -3547,7 +3559,7 @@ function eval_hash() {
if (hash0 && window.og_fn) {
var all = msel.getall(), mi;
for (var a = 0; a < all.length; a++)
if (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) {
if (og_fn == all[a].fn) {
mi = all[a];
break;
}
@@ -3760,7 +3772,7 @@ function sortfiles(nodes) {
if ((v + '').indexOf('<a ') === 0)
v = v.split('>')[1];
else if (name == "href" && v)
v = uricom_dec(v);
v = uri2txt(v, true);
nodes[b]._sv = v
}
@@ -4053,6 +4065,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>',
@@ -4073,13 +4090,13 @@ var fileman = (function () {
'<tr><td>perms</td><td class="sh_axs">',
];
for (var a = 0; a < perms.length; a++)
if (!has(['admin', 'move', 'delete'], 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');
html.push('</td></tr><table></div>');
shui.innerHTML = html.join('\n');
var sh_rand = ebi('sh_rand'),
@@ -4237,9 +4254,6 @@ var fileman = (function () {
s2d[a] = all.indexOf(sel[a]);
var vp = sel[a].vp;
if (vp.endsWith('/'))
vp = vp.slice(0, -1);
var vsp = vsplit(vp);
if (base != vsp[0])
return toast.err(0, esc('bug:\n' + base + '\n' + vsp[0]));
@@ -4533,8 +4547,9 @@ var fileman = (function () {
}
var msg = esc(L.fr_busy.format(f.length, f[0].ofn));
msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>';
msg += '\n<a id="fs_abrt" class="btn" href="#">' + L.fs_abrt + '</a>';
toast.show('inf r', 0, msg);
ebi('fs_abrt').onclick = fs_abrt;
var dst = base + uricom_enc(f[0].inew.value, false);
function rename_cb() {
@@ -4557,6 +4572,8 @@ var fileman = (function () {
xhr.onload = xhr.onerror = rename_cb;
xhr.send();
}
ire.oninput();
};
r.delete = function (e) {
@@ -4583,6 +4600,9 @@ var fileman = (function () {
if (err !== 'xbd')
toast.ok(2, L.fd_ok);
if (QS('#seldoc.sel'))
thegrid.setvis(true);
treectl.goto();
return;
}
@@ -4861,8 +4881,9 @@ var fileman = (function () {
return paster();
var msg = esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src)));
msg += '\n<a id="fs_abrt" class="btn" href="#" onclick="fs_abrt()">' + L.fs_abrt + '</a>';
msg += '\n<a id="fs_abrt" class="btn" href="#">' + L.fs_abrt + '</a>';
toast.show('inf r', 0, msg);
ebi('fs_abrt').onclick = fs_abrt;
var xhr = new XHR(),
act = r.ccp ? '?copy=' : '?move=',
@@ -5058,11 +5079,22 @@ var showfile = (function () {
r.nmap = {
'dockerfile': 'docker'
};
r.pmap = {
'sh': 'bash',
'ash': 'bash',
'dash': 'bash',
'luajit': 'lua',
'node': 'js',
};
var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nasm nim nix py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig';
x = x.split(/ +/g);
for (var a = 0; a < x.length; a++)
if (!r.map["." + x[a]])
r.map["." + x[a]] = x[a];
x = 'awk bash lua make perl python ruby swift';
x = x.split(/ +/g);
for (var a = 0; a < x.length; a++)
r.pmap[x[a]] = x[a];
r.sname = function (srch) {
return srch.split(/[?&]doc=/)[1].split('&')[0];
@@ -5071,7 +5103,7 @@ var showfile = (function () {
if (window.og_fn) {
var ext = og_fn.split(/\./g).pop();
if (r.map['.' + ext])
hist_replace(get_evpath() + '?doc=' + og_fn);
hist_replace(get_evpath() + '?doc=' + uricom_enc(og_fn));
}
window.Prism = { 'manual': true };
@@ -5082,7 +5114,7 @@ var showfile = (function () {
var m = /[?&]doc=([^&]+)/.exec(location.search);
if (m) {
setTimeout(function () {
r.show(uricom_dec(m[1]), true);
r.show(m[1], true);
}, 1);
}
}
@@ -5102,11 +5134,17 @@ var showfile = (function () {
return !!/[?&]doc=/.exec(location.search);
};
r.getlang = function (fn) {
r.getlang = function (fn, txt) {
fn = fn.toLowerCase();
var ext = fn.slice(fn.lastIndexOf('.'));
return r.map[ext] || r.nmap[fn];
}
var prog = txt && txt.startsWith('#!') && txt.split('\n')[0].replace(
'#!/usr/bin/env', '').replace(
'#!/usr/bin/', '').replace(
'#!/bin/', '').trim().split(' ')[0];
if (prog)
prog = r.pmap[prog] || r.pmap[prog.replace(/[0-9].*/, '')];
return r.map[ext] || r.nmap[fn] || prog;
};
r.addlinks = function () {
r.files = [];
@@ -5119,7 +5157,7 @@ var showfile = (function () {
if (!lang)
continue;
r.files.push({ 'id': link.id, 'name': uricom_dec(fn) });
r.files.push({ 'id': link.id, 'name': link.fn });
var ah = ebi(link.id),
td = ah.closest('tr').getElementsByTagName('td')[0];
@@ -5255,7 +5293,7 @@ var showfile = (function () {
txt = doc[2],
name = url.split('?')[0].split('/').pop(),
tname = uricom_dec(name),
lang = r.getlang(name),
lang = r.getlang(name, txt),
is_md = lang == 'md';
ebi('files').style.display = ebi('gfiles').style.display = ebi('lazy').style.display = ebi('pro').style.display = ebi('epi').style.display = 'none';
@@ -5444,7 +5482,7 @@ var showfile = (function () {
sel = false;
for (var b = 0; b < sels.length; b++)
if (vsplit(sels[b].vp)[1] == lin)
if (sels[b].fn == lin)
sel = true;
clmod(lis[a], 'hl', lin == fn);
@@ -5467,7 +5505,7 @@ var showfile = (function () {
r.tgltail = function () {
if (!window.TextDecoderStream) {
bcfg_set('taildoc', r.taildoc = false);
return toast.err(10, L.tail_2old);
return toast.err(10, L.b2old + ';\n' + L.tail_2old);
}
r.show(r.url, true);
};
@@ -5678,8 +5716,8 @@ var thegrid = (function () {
fid = oth.getAttribute('id'),
aplay = ebi('a' + fid),
atext = ebi('t' + fid),
is_txt = atext && !/\.ts$/.test(href) && showfile.getlang(href),
is_img = img_re.test(href),
is_txt = atext && !is_img && !/\.ts$/.test(href) && showfile.getlang(href),
is_dir = href.endsWith('/'),
is_srch = !!ebi('unsearch'),
in_tree = is_dir && treectl.find(oth.textContent.slice(0, -1)),
@@ -5885,10 +5923,15 @@ var thegrid = (function () {
html.push('<a href="' + ohref + '" ref="' + ref +
'"' + ac + ' ttt="' + esc(name) + '"><img style="height:' +
(r.sz / 1.25) + 'em" loading="lazy" onload="th_onload(this)" src="' +
(r.sz / 1.25) + 'em" loading="lazy" fetchPriority="low" src="' +
ihref + '" /><span' + ac + '>' + ao.innerHTML + '</span></a>');
}
ggrid.innerHTML = html.join('\n');
var ths = QSA('#ggrid>a>img');
for (var a = 0, aa = ths.length; a < aa; a++)
ths[a].onload = th_onload;
clmod(ggrid, 'crop', r.crop);
clmod(ggrid, 'nocrop', !r.crop);
@@ -5899,7 +5942,7 @@ var thegrid = (function () {
if (srch && r.sel)
gsel.click();
var ths = QSA('#ggrid>a');
ths = QSA('#ggrid>a');
for (var a = 0, aa = ths.length; a < aa; a++) {
ths[a].ondblclick = gclick2;
ths[a].onclick = gclick1;
@@ -5934,7 +5977,11 @@ var thegrid = (function () {
},
onChange: function (i, maxIdx) {
if (this[i].imageElement) {
sethash('g' + this[i].imageElement.getAttribute('ref') + getsort());
var h = 'g' + this[i].imageElement.getAttribute('ref');
if (this[i].page)
h += ',p' + this[i].page;
h += getsort();
sethash(h);
}
}
});
@@ -6040,8 +6087,8 @@ var thegrid = (function () {
})();
function th_onload(el) {
el.style.height = '';
function th_onload() {
this.style.height = '';
}
@@ -6414,6 +6461,11 @@ var ahotkeys = function (e) {
if (k == 'F2')
return fileman.rename();
if (k == 'F4') {
enspin('t', 1);
return treectl.goto();
}
if (!treectl.hidden && (!sh || !thegrid.en)) {
if (kl == 'a')
return QS('#twig').click();
@@ -7665,6 +7717,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" target="_blank" name="' + hname + '">📄</a>';
if (tn.lead == '-')
tn.lead = '<a href="?doc=' + bhref + '" id="t' + id +
'" rel="nofollow" class="doc' + (lang ? ' bri' : '') +
@@ -7757,6 +7814,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;
@@ -7873,7 +7931,7 @@ var treectl = (function () {
delete res['a'];
var keys = Object.keys(res);
for (var a = 0; a < keys.length; a++)
keys[a] = [uricom_dec(keys[a]), keys[a]];
keys[a] = [uri2txt(keys[a]), keys[a]];
if (ENATSORT)
keys.sort(function (a, b) { return NATSORT.compare(a[0], b[0]); });
@@ -7961,12 +8019,12 @@ var treectl = (function () {
})();
function enspin(i) {
function enspin(i, ns) {
i = 'dlt_' + i;
if (ebi(i))
return;
var d = mknod('div', i, SPINNER);
d.className = 'dumb_loader_thing';
d.className = 'dumb_loader_thing' + ( ns? ' ns' : '');
if (SPINNER_CSS)
d.style.cssText = SPINNER_CSS;
document.body.appendChild(d);
@@ -8592,7 +8650,7 @@ var setfszf = (function () {
setck('cplng=' + lang);
freshen();
var t = L.tt == 'English' ? '' : Ls.eng.lang_set;
modal.confirm(L.lang_set + "\n\n" + t, location.reload.bind(location), null);
modal.confirm(L.lang_set + "\n\n" + t, relod, null);
}
freshen();
@@ -8729,6 +8787,7 @@ var msel = (function () {
item.id = links[a].getAttribute('id');
item.sel = clgot(links[a].closest('tr'), 'sel');
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
item.fn = uricom_dec(href.split('/').pop());
if (dk) {
var m = /[?&](k=[^&#]+)/.exec(qhref);
@@ -8843,7 +8902,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();
@@ -8985,6 +9044,13 @@ var msel = (function () {
return;
}
if (this.status == 405) {
tb.value = '';
sf.textContent = 'already existed';
treectl.goto(this.vp + uricom_enc(this.dn) + '/', true);
return tree_scrollto();
}
xhrchk(this, L.fd_xe1, L.fd_xe2);
if (this.status !== 201) {
@@ -9111,7 +9177,7 @@ var sandboxjs = (function () {
var ret = '',
busy = false,
url = SR + '/.cpr/w/util.js?_=' + TS,
tag = '<script src="' + url + '"></script>';
tag = '<script nonce="' + JS_NONCE + '" src="' + url + '"></script>';
return function () {
if (ret || busy)
@@ -9121,7 +9187,7 @@ var sandboxjs = (function () {
xhr.open('GET', url, true);
xhr.onload = function () {
if (this.status == 200)
ret = '<script>' + this.responseText + '</script>';
ret = '<script nonce="' + JS_NONCE + '">' + this.responseText + '</script>';
};
xhr.send();
busy = true;
@@ -9143,7 +9209,7 @@ function show_md(md, name, div, url, depth) {
if (depth) {
clmod(div, 'raw', 1);
div.textContent = "--[ " + name + " ]---------\r\n" + md;
return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : 'your browser is too old'));
return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : L.b2old));
}
wfp_debounce.n--;
@@ -9269,8 +9335,9 @@ function sandbox(tgt, rules, allow, cls, html) {
html = '<html class="iframe ' + document.documentElement.className +
'"><head><style>html{background:#eee;color:#000}</style><style>' + globalcss() +
'</style><base target="_parent"></head><body id="b" class="logue ' + cls + '">' + html +
'<script>' + env + '</script>' + sandboxjs() +
'<script>var d=document.documentElement,TS="' + TS + '",' +
'<script nonce="' + JS_NONCE + '">' + env + '</script>' + sandboxjs() +
'<script nonce="' + JS_NONCE + '">' +
'var d=document.documentElement,TS="' + TS + '",' +
'loc=new URL("' + location.href.split('?')[0] + '");' +
'function say(m){window.parent.postMessage(m,"*")};' +
'setTimeout(function(){var its=0,pih=-1,f=function(){' +
@@ -9295,6 +9362,7 @@ function sandbox(tgt, rules, allow, cls, html) {
fr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : '');
fr.setAttribute('allow', allow);
fr.setAttribute('srcdoc', html);
tgt.innerHTML = '';
tgt.appendChild(fr);
treectl.sb_msg = true;
return true;
@@ -9790,6 +9858,7 @@ var rcm = (function () {
case 'pla': play('f-' + selFile.id); break;
case 'txt': showfile.show(selFile.name); break;
case 'md': location = selFile.path + (has(selFile.path, '?') ? '&v' : '?v'); break;
case 'wopi': window.open('?wopi=' + selFile.name, '_blank').focus(); break;
case 'cpl': cliptxt(selFile.url, function() {toast.ok(2, L.clipped)}); break;
case 'dl': ebi('seldl').click(); break;
case 'zip': ebi('selzip').click(); break;
@@ -9850,13 +9919,20 @@ var rcm = (function () {
var has_sel = msel.getsel().length;
var has_clip = fileman.clip.length;
var ext = (function() {
if (!selFile.name) return null;
var di = selFile.name.lastIndexOf('.');
if (di < 1) return null;
return selFile.name.slice(di + 1).toLowerCase();
})();
clmod(ebi('ropn'), 'hide', !selFile.path);
clmod(ebi('rply'), 'hide', selFile.type != 'gf' && selFile.type != 'af');
clmod(ebi('rpla'), 'hide', selFile.type != 'gf');
clmod(ebi('rtxt'), 'hide', !selFile.id);
clmod(ebi('rs1'), 'hide', !selFile.path);
clmod(ebi('rmd'), 'hide', !selFile.name || selFile.name.slice(-3) != ".md");
clmod(ebi('rmd'), 'hide', !selFile.name || ext != "md");
clmod(ebi('rwopi'), 'hide', !selFile.name || !wopi_set || !wopi_set.has(ext));
clmod(ebi('rcpl'), 'hide', !selFile.path);
clmod(ebi('rdl'), 'hide', !has_sel);
clmod(ebi('rzip'), 'hide', !has_sel);
@@ -9988,19 +10064,19 @@ function reload_browser() {
var selbox = null;
var ttimer = null;
var lpdelay = 250;
var lpdelay = 250;
var mvthresh = 44;
function unbox() {
qsr('.selbox');
ebi('gfiles').style.removeProperty('pointer-events')
ebi('wrap').style.removeProperty('user-select')
if (selbox) {
console.log(selbox)
window.getSelection().removeAllRanges();
}
is_selma = false;
dragging = false;
fwrap = null;
@@ -10033,7 +10109,7 @@ function reload_browser() {
function sel_start(e) {
if (e.button !== 0 && e.type !== 'touchstart') return;
if (!thegrid.en || !treectl.dsel) return;
if (e.target.closest('#widget,#ops,.opview,.doc')) return;
if (e.target.closest('#widget,#ops,.opview,.doc,.bbox-open')) return;
if (e.target.closest('#gfiles'))
ebi('gfiles').style.userSelect = "none";
@@ -10043,7 +10119,7 @@ function reload_browser() {
starty = pos.y;
is_selma = true;
ttimer = null;
if (e.type === 'touchstart') {
ttimer = setTimeout(function() {
ttimer = null;
@@ -10051,7 +10127,7 @@ function reload_browser() {
}, lpdelay);
}
}
function start_drag() {
if (dragging) return;
@@ -10062,7 +10138,7 @@ function reload_browser() {
ebi('gfiles').style.pointerEvents = 'none';
}
function sel_move(e) {
if (!is_selma) return;
var pos = getpp(e);
@@ -10079,7 +10155,7 @@ function reload_browser() {
if (!dragging && dist > mvthresh && !window.getSelection().toString()) {
if (e.target instanceof Element)
fwrap = e.target.closest('#wrap');
if (fwrap)
if (fwrap)
fwrap.style.userSelect = 'none';
else return;
start_drag();
@@ -10128,7 +10204,7 @@ function reload_browser() {
}
});
}
dsel_init();
})();
@@ -10171,7 +10247,7 @@ var mpss = (function() {
var gain = afilt.ssg.gain;
var duration = ae.duration || 0;
var slimit = duration * (config.sthresh / 100);
var elimit = duration * (1 - (config.etresh / 100));
var in_limits = ae.currentTime < slimit || ae.currentTime > elimit;
+1 -1
View File
@@ -14,7 +14,7 @@
<p>sorry for the inconvenience</p>
</div>
<script>
<script nonce="{{ js_nonce }}">
setTimeout(function() {
document.getElementById('box').style.opacity = 1;
}, 500);
+5 -4
View File
@@ -36,9 +36,10 @@
{%- endif %}
</div>
<a href="#" id="repl">π</a>
<script>
<script nonce="{{ js_nonce }}">
var SR="{{ r }}",
JS_NONCE = "{{ js_nonce }}",
lang="{{ lang }}",
dfavico="{{ favico }}";
@@ -46,11 +47,11 @@ var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
</script>
+9 -14
View File
@@ -131,19 +131,14 @@ write markdown (most html is 🙆 too)
</div>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
var SR = "{{ r }}",
JS_NONCE = "{{ js_nonce }}",
CGV1 = {{ cgv1 }},
last_modified = {{ lastmod }},
have_emp = {{ have_emp }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}";
var md_opt = {
link_md_as_html: false,
modpoll_freq: {{ md_chk_rate }}
};
(function () {
var l = window.localStorage,
drk = (l && l.light) != 1,
@@ -159,13 +154,13 @@ try { l.light = drk? 0:1; } catch (ex) { }
})();
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/md.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/md.js?_={{ ts }}"></script>
{%- if edit %}
<script src="{{ r }}/.cpr/w/md2.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/md2.js?_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_MD","md");
@@ -174,7 +169,7 @@ try { l.light = drk? 0:1; } catch (ex) { }
{%- endif %}
</script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
</body>
</html>
+5 -2
View File
@@ -316,8 +316,8 @@ function Modpoll() {
};
setTimeout(r.periodic, 300);
if (md_opt.modpoll_freq > 0)
setInterval(r.periodic, 1000 * md_opt.modpoll_freq);
if (modpoll_freq > 0)
setInterval(r.periodic, 1000 * modpoll_freq);
return r;
}
@@ -325,6 +325,9 @@ var modpoll = new Modpoll();
window.onbeforeunload = function (e) {
if (window.gtfo)
return;
if ((ebi("save").className + '').indexOf('disabled') >= 0)
return; //nice (todo)
+9 -14
View File
@@ -28,19 +28,14 @@
</div>
</div>
<a href="#" id="repl">π</a>
<script>
<script nonce="{{ js_nonce }}">
var SR = "{{ r }}",
JS_NONCE = "{{ js_nonce }}",
CGV1 = {{ cgv1 }},
last_modified = {{ lastmod }},
have_emp = {{ have_emp }},
md_no_br = {{ md_no_br }},
dfavico = "{{ favico }}";
var md_opt = {
link_md_as_html: false,
modpoll_freq: {{ md_chk_rate }}
};
var lightswitch = (function () {
var l = window.localStorage,
drk = (l && l.light) != 1,
@@ -54,14 +49,14 @@ try { l.light = drk? 0:1; } catch (ex) { }
})();
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/deps/easymde.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/mde.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/deps/marked.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/deps/easymde.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/mde.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_MDE","mde");
+3 -3
View File
@@ -40,19 +40,19 @@ a{color:#fc5}</style>
{%- endif %}
{%- if click %}
<script>document.getElementsByTagName("a")[0].click()</script>
<script nonce="{{ js_nonce }}">document.getElementsByTagName("a")[0].click()</script>
{%- endif %}
</div>
{%- if redir %}
<script>
<script nonce="{{ js_nonce }}">
setTimeout(function() {
location.replace("{{ redir }}");
}, 800);
</script>
{%- endif %}
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
</body>
+7 -6
View File
@@ -22,9 +22,10 @@
<div id="tw"></div>
</div>
<a href="#" id="repl">π</a>
<script>
<script nonce="{{ js_nonce }}">
var SR="{{ r }}",
JS_NONCE = "{{ js_nonce }}",
lang="{{ lang }}",
dutc={{ this.args.js_utc }},
dfavico="{{ favico }}";
@@ -33,13 +34,13 @@ var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script>var V={{ v }};</script>
<script src="{{ r }}/.cpr/w/rups.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}">var V={{ v }};</script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/rups.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_RUP","rups");
+6 -5
View File
@@ -62,9 +62,10 @@
{%- endif %}
</div>
<a href="#" id="repl">π</a>
<script>
<script nonce="{{ js_nonce }}">
var SR="{{ r }}",
JS_NONCE = "{{ js_nonce }}",
shr="{{ shr }}",
lang="{{ lang }}",
dutc={{ this.args.js_utc }},
@@ -74,12 +75,12 @@ var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/shares.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/shares.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_SHR","shares");
+13 -7
View File
@@ -172,7 +172,7 @@
<a id="ag" href="{{ r }}/?idp">view idp cache</a><br />
{%- endif %}
<a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a><br />
<a id="k" href="{{ r }}/?reset" class="r">reset client settings</a><br />
{%- if this.uname != '*' and not in_shr %}
<form method="post" enctype="multipart/form-data">
@@ -206,25 +206,31 @@
{%- if not this.args.nb %}
<span id="pb"><span>powered by</span> <a href="{{ this.args.pb_url }}">copyparty {{ver}}</a></span>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
var SR="{{ r }}",
JS_NONCE = "{{ js_nonce }}",
lang="{{ lang }}",
dfavico="{{ favico }}";
var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
document.getElementById('k').onclick = function () {
localStorage.clear();
return true;
};
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
{%- if lang != "eng" %}
<script src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/tl/{{ lang }}.js?_={{ ts }}"></script>
{%- endif %}
<script src="{{ r }}/.cpr/w/splash.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/splash.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_SPL","splash");
+16 -15
View File
@@ -54,7 +54,7 @@
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-dav:{{ rvp }} <b>W:</b>
</pre>
<ul>
@@ -77,7 +77,7 @@
<div class="os lin">
<p>rclone (v1.63 or later) is recommended:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone config create --obscure {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
</pre>
<ul>
@@ -145,9 +145,9 @@
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftp:{{ rvp }} <b>W:</b>
</pre>
@@ -156,9 +156,9 @@
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftps:{{ rvp }} <b>W:</b>
</pre>
@@ -184,9 +184,9 @@
<p>connect with plaintext FTP:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
{%- else %}
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
rclone config create --obscure {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
</pre>
@@ -195,9 +195,9 @@
<p>connect with TLS-encrypted FTPS:</p>
<pre>
{%- if un %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
{%- else %}
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
rclone config create --obscure {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
{%- endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
</pre>
@@ -347,9 +347,10 @@
</div>
<a href="#" id="repl">π</a>
<script>
<script nonce="{{ js_nonce }}">
var SR="{{ r }}",
JS_NONCE = "{{ js_nonce }}",
lang="{{ lang }}",
dfavico="{{ favico }}";
@@ -357,12 +358,12 @@ var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ args.theme }}";
</script>
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script src="{{ r }}/.cpr/w/svcs.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ r }}/.cpr/w/svcs.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
<script nonce="{{ js_nonce }}" src="{{ js }}_={{ ts }}"></script>
{%- endif %}
<script>
<script nonce="{{ js_nonce }}">
Date.now();function jsldp(a,b){2!=window[a]&&alert("FATAL ERROR: cannot load "+b+".js due to unreliable network or broken reverse-proxy; try CTRL-SHIFT-R")}
jsldp("J_UTL","util");
jsldp("J_SVC","svcs");
+4 -1
View File
@@ -39,6 +39,7 @@ Ls.chi = {
["ctrl-V", "粘贴(移动/复制)到此处"],
["Y", "下载选中项"],
["F2", "重命名选中项"],
["F4", "刷新"], //m
"文件列表选择",
["space", "切换文件选择"],
@@ -573,9 +574,10 @@ Ls.chi = {
"u_https1": "你应该",
"u_https2": "切换到 https",
"u_https3": "以获得更好的性能",
"u_ancient": '你还在用远古浏览器,也许你应该 <a href="#" onclick="goto(\'bup\')">改用 bup</a>',
"u_ancient": '你还在用远古浏览器,也许你应该 <a href="#" id="u2nah">改用 bup</a>',
"u_nowork": "需要 Firefox 53+ 或 Chrome 57+ 或 iOS 11+",
"tail_2old": "需要 Firefox 105+ 或 Chrome 71+ 或 iOS 14.5+",
"b2old": '浏览器版本太低', //m
"u_nodrop": '浏览器版本低,不支持通过拖动文件到窗口来上传文件',
"u_notdir": "这不是文件夹!\n\n你的浏览器太老了,\n请尝试将文件夹拖入窗口",
"u_uri": "要从其他浏览器窗口拖放图片,\n请将其拖放到大上传按钮上",
@@ -659,6 +661,7 @@ Ls.chi = {
"rc_pla": "作为音频播放",
"rc_txt": "在文本文件查看器中打开",
"rc_md": "在 markdown 查看器中打开",
"rc_wopi": "在 office 编辑器中打开", //m
"rc_dl": "下载",
"rc_zip": "下载为压缩包",
"rc_cpl": "复制链接",
+4 -1
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"],
@@ -576,9 +577,10 @@ Ls.cze = {
"u_https1": "měli byste",
"u_https2": "přejít na https",
"u_https3": "pro lepší výkon",
"u_ancient": "váš prohlížeč je úctyhodně starý -- možná byste měli <a href=\"#\" onclick=\"goto('bup')\">použít bup</a>",
"u_ancient": "váš prohlížeč je úctyhodně starý -- možná byste měli <a href=\"#\" id=\"u2nah\">použít bup</a>",
"u_nowork": "vyžadován firefox 53+ nebo chrome 57+ nebo iOS 11+",
"tail_2old": "vyžadován firefox 105+ nebo chrome 71+ nebo iOS 14.5+",
"b2old": "váš prohlížeč je příliš starý", //m
"u_nodrop": "váš prohlížeč je příliš starý pro nahrávání přetažením (drag-and-drop)",
"u_notdir": "toto není složka!\n\nváš prohlížeč je příliš starý,\nzkuste prosím soubory přetáhnout",
"u_uri": "pro přetažení obrázků z jiných oken prohlížeče,\nje prosím přetáhněte na velké tlačítko pro nahrávání",
@@ -662,6 +664,7 @@ Ls.cze = {
"rc_pla": "přehrát jako zvuk", //m
"rc_txt": "otevřít v prohlížeči souborů", //m
"rc_md": "otevřít v textovém editoru", //m
"rc_wopi": "otevřít v office editoru", //m
"rc_dl": "stáhnout", //m
"rc_zip": "stáhnout jako archiv", //m
"rc_cpl": "kopírovat odkaz", //m
+4 -1
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"],
@@ -572,9 +573,10 @@ Ls.deu = {
"u_https1": "für bessere Performance solltest du",
"u_https2": "auf HTTPS wechseln",
"u_https3": " ",
"u_ancient": 'Dein Browser ist verdammt antik -- vielleicht solltest du <a href="#" onclick="goto(\'bup\')">stattdessen bup benutzen</a>',
"u_ancient": 'Dein Browser ist verdammt antik -- vielleicht solltest du <a href="#" id="u2nah">stattdessen bup benutzen</a>',
"u_nowork": "Benötigt Firefox 53+ oder Chrome 57+ oder iOS 11+",
"tail_2old": "Benötigt Firefox 105+ oder Chrome 71+ oder iOS 14.5+",
"b2old": 'Dein Browser ist zu alt', //m
"u_nodrop": 'Dein Browser ist zu alt für Drag-and-Drop Uploads',
"u_notdir": "Das ist kein Ordner!\n\nDein Browser ist zu alt,\nversuch stattdessen dragdrop",
"u_uri": "Um Bilder per Drag-and-Drop aus anderen Browserfenstern hochzuladen,\nlass' sie bitte über dem grossen Upload-Button fallen",
@@ -658,6 +660,7 @@ Ls.deu = {
"rc_pla": "als Audio abspielen",
"rc_txt": "als Text öffnen",
"rc_md": "im Texteditor öffnen",
"rc_wopi": "im office-editor öffnen", //m
"rc_dl": "herunterladen",
"rc_zip": "als Archiv herunterladen",
"rc_cpl": "Link kopieren", //m
+4 -1
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"],
@@ -572,9 +573,10 @@ Ls.epo = {
"u_https1": "vi devas",
"u_https2": "ŝalti HTTPS-protokolon",
"u_https3": "por pli bona rendimento",
"u_ancient": 'via retumilo estas vere antikva -- eble vi devus <a href="#" onclick="goto(\'bup\')">uzi alŝutilon bup anstataŭe</a>',
"u_ancient": 'via retumilo estas vere antikva -- eble vi devus <a href="#" id="u2nah">uzi alŝutilon bup anstataŭe</a>',
"u_nowork": "Firefox 53+ aŭ Chrome 57+ aŭ iOS 11+ necesas",
"tail_2old": "Firefox 105+ aŭ Chrome 71+ aŭ iOS 14.5+ necesas",
"b2old": 'via retumilo estas tro malnova', //m
"u_nodrop": 'via retumilo estas tro malnova por ŝova-kaj-demeta alŝutado',
"u_notdir": "tio ne estas dosierujo!\n\nvia retumilo estas tro malnova,\nbonvolu ŝovu kaj demetu anstataŭe",
"u_uri": "por ŝovi-kaj-demeti bildon de aliaj fenestroj de retumiloj,\nbonvolu demeti ĝin sur la grandan alŝut-butonon",
@@ -658,6 +660,7 @@ Ls.epo = {
"rc_pla": "ludi kiel aŭdiaĵo",
"rc_txt": "malfermi per tekstovidilo",
"rc_md": "malfermi per tekstoredaktilo",
"rc_wopi": "malfermi per office-redaktilo", //m
"rc_dl": "elŝuti",
"rc_zip": "elŝuti kiel arkivo",
"rc_cpl": "kopii ligilon",
+4 -1
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"],
@@ -572,9 +573,10 @@ Ls.fin = {
"u_https1": "sinun kannattaisi",
"u_https2": "vaihtaa https:ään",
"u_https3": "paremman suorituskyvyn vuoksi",
"u_ancient": 'selaimesi on ns. vaikuttavan ikivanha -- kannattais varmaan <a href="#" onclick="goto(\'bup\')">käyttää bup:ia tän sijaan</a>',
"u_ancient": 'selaimesi on ns. vaikuttavan ikivanha -- kannattais varmaan <a href="#" id="u2nah">käyttää bup:ia tän sijaan</a>',
"u_nowork": "tarvitaan firefox 53+ tai chrome 57+ tai iOS 11+",
"tail_2old": "tarvitaan firefox 105+ tai chrome 71+ tai iOS 14.5+",
"b2old": 'selaimesi on liian vanha', //m
"u_nodrop": 'selaimesi on liian vanha vedä-ja-pudota lataamiseen',
"u_notdir": "tuo ei ole hakemisto!\n\nselaimesi on liian vanha,\nkokeile sen sijaan 'vedä-pudota'-tekniikkaa.",
"u_uri": "'vedä-pudottaaksesi' kuvia muista selainikkunoista,\npudota se isoon latausnapppiin",
@@ -658,6 +660,7 @@ Ls.fin = {
"rc_pla": "toista äänitiedostona",
"rc_txt": "avaa tekstinäkymässä",
"rc_md": "avaa markdown-näkymässä",
"rc_wopi": "avaa office-editorissa", //m
"rc_dl": "lataa",
"rc_zip": "lataa arkistona",
"rc_cpl": "kopioi linkki",
+4 -1
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"],
@@ -572,9 +573,10 @@ Ls.fra = {
"u_https1": "vous devriez",
"u_https2": "passer à https",
"u_https3": "pour de meilleure performances",
"u_ancient": 'votre navigateur est impressionnamment ancien -- vous devriez peut-être <a href="#" onclick="goto(\'bup\')">utiliser bup à la place</a>',
"u_ancient": 'votre navigateur est impressionnamment ancien -- vous devriez peut-être <a href="#" id="u2nah">utiliser bup à la place</a>',
"u_nowork": "nécessite firefox 53+ ou chrome 57+ ou iOS 11+",
"tail_2old": "nécessite firefox 105+ ou chrome 71+ ou iOS 14.5+",
"b2old": 'votre navigateur est trop ancien', //m
"u_nodrop": 'votre navigateur est trop ancien pour le téléversement par glisser-déposer',
"u_notdir": "ce n'est pas un dossier!\n\nvotre navigateur est trop ancien,\nveuillez essayer le glisser-déposer à la place",
"u_uri": "pour glisser-déposer des images depuis d'autres fenêtres de navigateur,\nveuillez les déposer sur le gros bouton de téléversement",
@@ -658,6 +660,7 @@ Ls.fra = {
"rc_pla": "Lire comme audio", //m
"rc_txt": "ouvrir dans le visionneur de fichiers", //m
"rc_md": "ouvrir dans l’éditeur de texte", //m
"rc_wopi": "ouvrir dans l’éditeur office", //m
"rc_dl": "télécharger", //m
"rc_zip": "télécharger comme archive", //m
"rc_cpl": "copier le lien", //m
+4 -1
View File
@@ -39,6 +39,7 @@ Ls.grc = {
["ctrl-V", "επικόλληση (μετακίνηση/αντιγραφή) εδώ"],
["Y", "λήψη επιλεγμένων"],
["F2", "μετονομασία επιλεγμένων"],
["F4", "ανανέωση"], //m
"λίστα αρχείων",
["space", "εναλλαγή επιλογής αρχείου"],
@@ -572,9 +573,10 @@ Ls.grc = {
"u_https1": "πρέπει",
"u_https2": "μετάβαση σε https",
"u_https3": "για καλύτερη απόδοση",
"u_ancient": 'ο browser σου είναι εντυπωσιακά απαρχαιωμένος — ίσως να <a href="#" onclick="goto(\'bup\')">χρησιμοποιήσεις το bup αντί γι\' αυτό</a>',
"u_ancient": 'ο browser σου είναι εντυπωσιακά απαρχαιωμένος — ίσως να <a href="#" id="u2nah">χρησιμοποιήσεις το bup αντί γι\' αυτό</a>',
"u_nowork": "χρειάζεται firefox 53+ ή chrome 57+ ή iOS 11+",
"tail_2old": "χρειάζεται firefox 105+ ή chrome 71+ ή iOS 14.5+",
"b2old": 'ο browser σου είναι πολύ παλιός', //m
"u_nodrop": "ο browser σου είναι πολύ παλιός για drag&amp;drop μεταφορτώσεις",
"u_notdir": "αυτός δεν είναι φάκελος!\n\nο browser σου είναι πολύ παλιός,\nδοκίμασε drag&amp;drop αντ' αυτού",
"u_uri": "για να κάνεις drag&amp;drop εικόνων από άλλα παράθυρα browser,\nρίξ' τες πάνω στο μεγάλο κουμπί μεταφόρτωσης",
@@ -658,6 +660,7 @@ Ls.grc = {
"rc_pla": "αναπαραγωγή ως ήχος", //m
"rc_txt": "άνοιγμα στον προβολέα αρχείων", //m
"rc_md": "άνοιγμα στον επεξεργαστή κειμένου", //m
"rc_wopi": "άνοιγμα στον office επεξεργαστή", //m
"rc_dl": "λήψη", //m
"rc_zip": "λήψη ως αρχείο", //m
"rc_cpl": "ἀντίγραφε σύνδεσμον", //m
+4 -1
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'],
@@ -573,9 +574,10 @@ Ls.hun = {
"u_https1": 'érdemesebb lenne',
"u_https2": 'https-re váltani',
"u_https3": 'a jobb sebességért',
"u_ancient": 'lenyűgözően őskori a böngésződ -- talán <a href="#" onclick="goto(\'bup\')">használd inkább az egyszerű uploader-t</a>',
"u_ancient": 'lenyűgözően őskori a böngésződ -- talán <a href="#" id="u2nah">használd inkább az egyszerű uploader-t</a>',
"u_nowork": 'legalább Firefox 53, Chrome 57 vagy iOS 11 kell',
"tail_2old": 'legalább Firefox 105, Chrome 71 vagy iOS 14.5 kell',
"b2old": 'a böngésződ túl öreg', //m
"u_nodrop": 'a böngésződ túl öreg a drag-and-drop-hoz',
"u_notdir": 'ez nem egy mappa!\n\na böngésződ túl öreg,\npróbáld meg inkább behúzni',
"u_uri": 'képek behúzásához más ablakból,\ndobd rá a nagy feltöltés gombra',
@@ -659,6 +661,7 @@ Ls.hun = {
"rc_pla": 'lejátszás audióként',
"rc_txt": 'megnyitás szövegként',
"rc_md": 'megnyitás markdownként',
"rc_wopi": "megnyitás office szerkesztőben", //m
"rc_dl": 'letöltés',
"rc_zip": 'letöltés archívumként',
"rc_cpl": 'link másolása',
+4 -1
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"],
@@ -572,9 +573,10 @@ Ls.ita = {
"u_https1": "dovresti",
"u_https2": "passare a https",
"u_https3": "per prestazioni migliori",
"u_ancient": 'il tuo browser è incredibilmente antico -- forse dovresti <a href="#" onclick="goto(\'bup\')">usare bup invece</a>',
"u_ancient": 'il tuo browser è incredibilmente antico -- forse dovresti <a href="#" id="u2nah">usare bup invece</a>',
"u_nowork": "serve firefox 53+ o chrome 57+ o iOS 11+",
"tail_2old": "serve firefox 105+ o chrome 71+ o iOS 14.5+",
"b2old": 'il tuo browser è troppo vecchio', //m
"u_nodrop": 'il tuo browser è troppo vecchio per il caricamento drag-and-drop',
"u_notdir": "quella non è una cartella!\n\nil tuo browser è troppo vecchio,\nprova dragdrop invece",
"u_uri": "per trascinare immagini da altre finestre del browser,\nrilasciale sul pulsante upload grande",
@@ -658,6 +660,7 @@ Ls.ita = {
"rc_pla": "riproduci come audio", //m
"rc_txt": "apri nel visualizzatore di file", //m
"rc_md": "apri nell’editor di testo", //m
"rc_wopi": "apri nell’editor office", //m
"rc_dl": "scarica", //m
"rc_zip": "scarica come archivio", //m
"rc_cpl": "copia link", //m
+4 -1
View File
@@ -39,6 +39,7 @@ Ls.jpn = {
["ctrl-V", "ここに貼り付け(移動/コピー)"],
["Y", "選択した項目をダウンロード"],
["F2", "選択した項目の名前を変更"],
["F4", "更新"], //m
"file-list-sel",
["space", "ファイル選択の切り替え"],
@@ -572,9 +573,10 @@ Ls.jpn = {
"u_https1": "あなたはするべき",
"u_https2": "httpsに切り替える",
"u_https3": "パフォーマンス向上のため",
"u_ancient": '現在のブラウザは驚くほど古いです -- <a href="#" onclick="goto(\'bup\')">代わりにbup</a>を使った方がいいかもしれません',
"u_ancient": '現在のブラウザは驚くほど古いです -- <a href="#" id="u2nah">代わりにbup</a>を使った方がいいかもしれません',
"u_nowork": "Firefox 53以降、Chrome 57以降、またはiOS 11以降が必要です",
"tail_2old": "Firefox 105以降、Chrome 71以降、またはiOS 14.5以降が必要です",
"b2old": '現在のブラウザは古すぎます', //m
"u_nodrop": '現在のブラウザは古すぎてドラッグ&ドロップによるアップロードに対応していません',
"u_notdir": "それはフォルダではありません!\n\nブラウザが古すぎです、\n代わりにドラッグドロップを試してください",
"u_uri": "他のブラウザウィンドウから画像をドラッグアンドドロップするには、\n大きなアップロードボタンにドロップしてください",
@@ -658,6 +660,7 @@ Ls.jpn = {
"rc_pla": "オーディオとして再生",
"rc_txt": "ファイルビューアで開く",
"rc_md": "markdownエディターで開く",
"rc_wopi": "officeエディターで開く", //m
"rc_dl": "ダウンロード",
"rc_zip": "圧縮してダウンロード",
"rc_cpl": "リンクをコピー",
+4 -1
View File
@@ -39,6 +39,7 @@ Ls.kor = {
["ctrl-V", "여기에 붙여넣기 (이동/복사)"],
["Y", "선택 항목 다운로드"],
["F2", "선택 항목 이름 바꾸기"],
["F4", "새로 고침"], //m
"파일 목록 선택",
["space", "파일 선택/해제"],
@@ -572,9 +573,10 @@ Ls.kor = {
"u_https1": "더 나은 성능을 위해",
"u_https2": "https로 전환",
"u_https3": "하는 것이 좋습니다",
"u_ancient": '브라우저가 정말 오래되었네요 -- 아마도 <a href="#" onclick="goto(\'bup\')">bup을 대신 사용</a>해야 할 것 같습니다',
"u_ancient": '브라우저가 정말 오래되었네요 -- 아마도 <a href="#" id="u2nah">bup을 대신 사용</a>해야 할 것 같습니다',
"u_nowork": "Firefox 53+, Chrome 57+ 또는 iOS 11+가 필요합니다",
"tail_2old": "Firefox 105+, Chrome 71+ 또는 iOS 14.5+가 필요합니다",
"b2old": '브라우저가 너무 오래되었습니다', //m
"u_nodrop": '브라우저가 너무 오래되어 드래그 앤 드롭 업로드를 지원하지 않습니다',
"u_notdir": '폴더가 아닙니다!\n\n브라우저가 너무 오래되었습니다,\n대신 드래그드롭을 시도해보세요',
"u_uri": '다른 브라우저 창에서 이미지를 드래그드롭하려면,\n큰 업로드 버튼 위로 떨어뜨려주세요',
@@ -658,6 +660,7 @@ Ls.kor = {
"rc_pla": "오디오로 재생", //m
"rc_txt": "파일 뷰어에서 열기", //m
"rc_md": "텍스트 편집기에서 열기", //m
"rc_wopi": "office 편집기에서 열기", //m
"rc_dl": "다운로드", //m
"rc_zip": "압축 파일로 다운로드", //m
"rc_cpl": "링크 복사", //m
+4 -1
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"],
@@ -572,9 +573,10 @@ Ls.nld = {
"u_https1": "Je moet",
"u_https2": "overschakelen naar https",
"u_https3": "voor betere prestaties",
"u_ancient": 'Je browser is indrukwekkend oud -- misschien moet je <a href="#" onclick="goto(\'bup\')">in plaats daarvan bup gebruiken</a>',
"u_ancient": 'Je browser is indrukwekkend oud -- misschien moet je <a href="#" id="u2nah">in plaats daarvan bup gebruiken</a>',
"u_nowork": "Je moet firefox 53+ of chrome 57+ of iOS 11+ hebben",
"tail_2old": "Je moet firefox 105+ of chrome 71+ of iOS 14.5+ hebben",
"b2old": 'Je browser is te oud', //m
"u_nodrop": 'Je browser is te oud voor uploaden via slepen en neerzetten',
"u_notdir": "Dat is geen map!\n\nuw browser is te oud,\nprobeer in plaats daarvan sleep en neerzetten",
"u_uri": "Om afbeeldingen te slepen vanuit andere browser tabblad,\nplaats deze dan op de grote uploadknop",
@@ -658,6 +660,7 @@ Ls.nld = {
"rc_pla": "Afspelen als audio", //m
"rc_txt": "Openen in bestandsviewer", //m
"rc_md": "Openen in teksteditor", //m
"rc_wopi": "Openen in office-editor", //m
"rc_dl": "Downloaden", //m
"rc_zip": "Downloaden als archief", //m
"rc_cpl": "Link kopiëren", //m
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff. Show more