Compare commits

..

217 commits

Author SHA1 Message Date
ed e7f2c6d806 update pkgs to 1.19.2 2025-08-17 11:51:56 +00:00
ed a113d3b925 v1.19.2 2025-08-17 11:21:25 +00:00
ed 96cb5abf53 extend vips heif formats 2025-08-17 11:07:02 +00:00
ed 55c85d0984 docker: add rawpy to iv/dj 2025-08-17 11:06:38 +00:00
ed 782e2f1de3 bbox: stay fullscreen 2025-08-17 10:42:45 +00:00
ed f4727f8ea3 fix config expansion order; closes #556 2025-08-17 10:05:25 +00:00
ed d4cf42e760 show severity in logs with no-ansi; #616 2025-08-17 09:06:23 +00:00
ed 98d117b8ad music-thumbs: use embedded art as default (closes #252);
previous behavior can be restored with --th-spec-p 2

thumbnails cache (.hist/th/) must be deleted to take effect
2025-08-16 23:00:15 +00:00
ed d9046f7e01 fix xvol false-positive;
given the following config:

* volume /a mapped to /srv/nas/
* volume /b mapped to /srv/nas/foo/
* anyone can read volume /a but not /b

accessing /a/foo/ would incorrectly fail because the xvol-check
would select /b based on its abspath being physically closer,
not considering that the same abspath is reachable from /a
2025-08-16 21:55:51 +00:00
ed dcc6b1b4ef fix download-selection in old firefox; closes #618 2025-08-16 21:54:45 +00:00
ed 274c074775 hide --rp-loc in tree; closes #306 2025-08-16 20:45:46 +00:00
Toast 187cae25bf
Build nix packages from source (#253)
* nix: get source tarball with update.py

* nix: build from source

nix: remove u2c and partyfuse packages
The main copyparty package has u2c and partyfuse, so these packages are
redundant now

nix: add fusepy dependency

fix: nix:  use replace pyfuse with fusepy

* nix: fix extra python packages

* nix: add optional dependencies

* nix: add partftpy package

* nix: add tftp parameter to package

* nix: enable pyproject for partftpy package

* nix: replace partftpy overlay with real package

* nix: add updater for partftpy

* nix: bring back local release pin to update.py

nix: update local release pin function in update.py

---------

Signed-off-by: Toast <39011842+toast003@users.noreply.github.com>
2025-08-16 06:29:03 +02:00
ed 43a19779c1 ftp: fix potential utime issue; closes #539 2025-08-15 21:36:02 +00:00
ed 23ea1c8a14 better dropdown color 2025-08-15 21:35:10 +00:00
ed e3c7d6776e fix test 2025-08-15 21:34:56 +00:00
ed 4df033ecc3 [DB-V6]: store usernames; closes #530 2025-08-15 21:33:13 +00:00
ed 1228b5510b option -ss requires webdav login; closes #613 2025-08-15 20:14:35 +00:00
ed 62e072a2ed restrict account to ip/subnet; closes #397 2025-08-15 20:12:17 +00:00
ed a4649d1e71 generic header auth (closes #504);
extends idp-auth to also accept a collection of headers (and
expected values of those headers) and map those to certain users

useful for Tailscale-User-Login and similar
2025-08-15 19:19:21 +00:00
ed f4a3fba29c add global-logout button 2025-08-14 21:53:57 +00:00
ed 3aa8b7aa2d ftp: reject uploads nicely; closes #573
if a client tries to upload where it does not have write-access,
rather than kicking the client with an exception, reply properly
2025-08-14 20:31:58 +00:00
ed d56230573d separate audio-transcode timeout (#598) 2025-08-14 20:02:32 +00:00
ed af8620da92 fix --lang helptext; closes #594 2025-08-14 19:50:50 +00:00
ed 2961dea5bb tl cleanup 2025-08-14 19:36:20 +00:00
nyqui 4e878d2f1e
initial Korean translation (#583)
Signed-off-by: nyqui <67160376+nyqui@users.noreply.github.com>
2025-08-14 19:30:41 +00:00
ed 7f44875061 autogen pw for blank-pw users (closes #596);
if a user is defined with a blank password,
generate a strong password for that user
2025-08-14 19:22:04 +00:00
ed 68907eaf48 add "@acct", a group with all authed users; closes #604 2025-08-14 19:11:57 +00:00
ed c4a4fddd27 translations: OK/Cancel; closes #599 2025-08-14 18:36:31 +00:00
ed 5b62742512 run from src with py3.9 2025-08-14 18:10:55 +00:00
ed 554cc2f3ee support xdev/xvol in rootless vfs; closes #603 2025-08-14 18:09:23 +00:00
ed 6303effe59 configurable max num cookies 2025-08-14 17:49:48 +00:00
ed 659f351c65 support pillow-heif; closes #607 2025-08-14 16:42:48 +00:00
Bevinsky d676a86f3f
Add Swedish translation (#551) 2025-08-13 22:50:15 +00:00
ed 715d374ee4 button to abort copy/move; closes #572 2025-08-12 21:46:42 +00:00
Danny Piper c9fd608732 feat: added cr3 to raw formats list
Signed-off-by: Danny Piper <djpiper28@gmail.com>
2025-08-12 22:19:54 +02:00
ed c32a672a68 fix transcoding tooltips; closes #580 2025-08-12 19:34:44 +00:00
ed 69d9878acd use the good name 2025-08-12 19:03:34 +00:00
varphi d8662aeb0e
add win95 light and dark themes (#581) 2025-08-12 19:01:41 +00:00
mat a407eb9269 warn if failed to import pyvips
pyvips uses `ffi.callback()`, which fails on on some systems
2025-08-12 20:15:26 +02:00
ed 1ebe06f51e sticky qr-code; #533 2025-08-11 20:49:09 +00:00
ed 88243ac8d6 make-rpm: small tweaks;
* fail fast on error
* ensure all deps

Signed-off-by: ed <s@ocv.me>
2025-08-11 20:36:14 +02:00
Kamalei Zestri 6ccc9224f3 make rpm 2025-08-11 20:36:14 +02:00
Adam R. Nelson 0177a9b402
Add RAW file thumbnailing support via rawpy (#567)
* add RAW image file types to mimetype list
* add RAW thumbnailer via rawpy

---------

Signed-off-by: Adam R. Nelson <adam@nels.onl>
Signed-off-by: ed <s@ocv.me>
2025-08-11 17:28:01 +00:00
AppleTheGolden 9435e6b2e2
EPUB Thumbnailing support (#561)
* EPUB Thumbnailing support

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-08-10 20:30:37 +00:00
Bevinsky 0da93659a4 Add missing translated string in up2k. 2025-08-10 22:04:09 +02:00
ed db2a03409c update pkgs to 1.19.1 2025-08-10 12:44:51 +00:00
ed c2cee222bd v1.19.1 2025-08-10 12:26:12 +00:00
ed b87f8f1b01 french improvements by @Equinoxs #553 2025-08-10 12:10:54 +00:00
ed a01870b744 avoid macos bug (finder hangs on connect) 2025-08-10 13:55:52 +02:00
ed 3560eeb10e better with sessions 2025-08-10 13:55:29 +02:00
ed 03acd65e96 avoid ios bug (keystore spam) 2025-08-10 11:45:20 +00:00
ed e5e822951d fix filter case-sensitivity 2025-08-10 11:31:12 +00:00
ed 347cf6a546 fix dropdown color 2025-08-10 11:28:22 +00:00
ed 8ba98877ee patch pyftpdlib, fixes #539
upgrading pyftpdlib brings only pain and no benefits
so grafting a patch for this instead
2025-08-10 11:23:22 +00:00
ed 3c78c6a880 custom mdns domain, closes #549 2025-08-10 10:07:41 +00:00
Andrew Lee 7aa21483c5
French translation part 2: splash.js #553 2025-08-10 10:06:23 +00:00
icxes 074e106e24 fix PRTY_CONFIG not reading global flags from the config 2025-08-10 12:02:44 +02:00
Tr3yWay996 e9ddfccfb6
Add French translation (#486)
Add French translation (#486)

---------

Signed-off-by: ed <s@ocv.me>
Co-authored-by: Packingdustry <alois.mc@hotmail.com>
Co-authored-by: Andrew Lee <andrew@alee14.me>
Co-authored-by: A. Jakubiak <contact@jakubiak.fr>
2025-08-09 20:26:52 +00:00
Chloe Surett 91ce7a29aa
Add .idea to .gitignore (#547) 2025-08-09 20:17:20 +00:00
Artur Borecki 392a4db55b
Add Polish translation (#463)
Add Polish translation

---------

Signed-off-by: Artur Borecki <me@pufereq.pl>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: dai <contact@daimond113.com>
Co-authored-by: ed <s@ocv.me>
2025-08-08 22:55:57 +00:00
Kent Daleng 3931bc2779
legg åt nynorskoversetjing (#537)
* legg åt nynorskoversetjing, og fikser et par typos på bokmål også :)
* add nno to splash, fix a few more stray typos
* more til -> åt

---------

Signed-off-by: Kent D <lolexplode@gmail.com>
2025-08-08 22:54:24 +02:00
chamdim bd514f0666
Greek: fix typos (#529)
Signed-off-by: chamdim <94919340+chamdim@users.noreply.github.com>
2025-08-08 09:54:27 +02:00
chamdim f8a7c02f23
Greek: fix typo (#528)
Signed-off-by: chamdim <94919340+chamdim@users.noreply.github.com>
2025-08-08 09:54:11 +02:00
Vlad 0dd5987250
add Ukrainian translations for control panel (#525) 2025-08-08 09:53:50 +02:00
ed 4eca4885f3 update pkgs to 1.19.0 2025-08-07 22:43:08 +00:00
ed e9ecb2edc5 v1.19.0 2025-08-07 22:13:52 +00:00
ed f0b1c82b44 i18n: support czech declensions 2025-08-07 22:09:11 +00:00
Jakub Pelc c955658332
Czech translation (#471)
* added czech translation
* add czech translations to splash

---------

Signed-off-by: Jakub Pelc <jakub.pelc@seznam.cz>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-08-07 21:48:32 +00:00
ed a98360f213 copyparty.exe: update to python 3.13.6 2025-08-07 21:30:55 +00:00
ed 33497e6b11 sfx: add english-only 2025-08-07 21:28:56 +00:00
ed 36ab323d08 sfx: simplify (remove bruteforce packing) 2025-08-07 21:26:59 +00:00
ed 1bf23fabc6 unames: fix shares 2025-08-07 20:59:41 +00:00
ed d9e3f998d1 fix zipmaxu (it did nothing) 2025-08-07 20:53:25 +00:00
ed 1b71294aab close #387 2025-08-07 20:30:09 +00:00
ed 346515ccf1 add optional username login; closes #511 2025-08-07 20:29:44 +00:00
icxes 3c42a34f7b update PKGBUILD; remove prisonparty user service 2025-08-07 19:59:42 +00:00
icxes 13499d2846 remove prisonparty-user.service
there is no real point to a prisonparty user service, as chroot requires root
2025-08-07 19:59:42 +00:00
ed 8b31ed8816 text-editor: optional EOL conversion; closes #513 2025-08-07 19:11:28 +00:00
ed bcc3b1568e add qrcode to connectpage; closes #523 2025-08-07 18:39:22 +00:00
ed 2943c7f2d5 move the docker config smoketest over here 2025-08-07 18:30:40 +00:00
exci 34d98e9980
add systemd user services and templated services (#502)
* move service files from contrib/package/arch/ to /contrib/systemd/
* add simpler default copyparty.conf that puts users in jail
* remove warning about .conf files in ~/.config/copyparty/
* update PKGBUILD with changes
* add links to configuration examples in index.md
* fix link to the example config
* update README.md arch instructions

---------

Co-authored-by: icxes <icxes@dev.null@need.moe>
Co-authored-by: ed <s@ocv.me>
2025-08-07 18:10:26 +00:00
ed 9e980bb552 try to detect proxies with misbehaving caches (#488) 2025-08-07 17:57:10 +00:00
ed 3f8cb7e877 xff: require explicit configuration of --rproxy;
try to avoid dangerous misconfiguration of how to determine the
client's IP by more aggressively asking for the correct config;

if the --xff-hdr (default: x-forwarded-for) appears in a request
then it will now be ignored unless --rproxy says which IP to use
2025-08-07 17:00:42 +00:00
ed 4a04356814 fix non-e2d dupe finalizing;
when the up2k database is not enabled, only the
38400 most recent uploads are kept in memory serverside

the webui did not anticipate this, expecting the server to
finalize all dupes with just a single pass of brief handshakes

fix this by doing as many passes as necessary, only stopping if
a pass does not make any progress (filesystem-issues or some such)
2025-08-07 15:57:51 +00:00
ed 8a0746c6af fix viewing .MD files 2025-08-07 15:24:15 +00:00
ed 54caf63f6a optimize 2025-08-07 15:18:59 +00:00
mati1210 9b9d2a92ca
support systemd socket activation (fd passing) (#515)
* add support for socket passing
* slight tweaks before merge

---------

Signed-off-by: mat <matheuz1210@gmail.com>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-08-07 15:00:53 +00:00
ed a9ee4f24d5 dropdowns for languages, themes, key-notation 2025-08-06 21:53:20 +00:00
ed 29a4e54799 tl cleanup 2025-08-06 21:10:15 +00:00
ed 3b26884c69 tl cleanup 2025-08-06 21:08:11 +00:00
ed 392abd0675 add greek splash tl; closes #493 2025-08-06 21:02:25 +00:00
chamdim 50f46187f1
Greek translation (#468)
* Greek translation
* Update browser.js
* greek: fixes before merge

---------

Signed-off-by: chamdim <94919340+chamdim@users.noreply.github.com>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-08-06 21:00:42 +00:00
ed 7ae84dea1a autorefresh controlpanel 2025-08-06 20:30:01 +00:00
ed a57f7cc2f8 ie9: recent-uploads 2025-08-06 20:25:29 +00:00
ed 0f55a1ae86 fix js-crash for url //; closes #487 2025-08-06 20:24:31 +00:00
ed 00cb1f74e2 golf 2025-08-06 20:23:39 +00:00
ed b664ebb01f add chungus.conf (#475) 2025-08-04 22:56:37 +00:00
ed c2ac57a2a8 improve helptext for multi-value options (#475) 2025-08-04 22:56:30 +00:00
geekalaa 0df1901fc0 fix: add missing L. prefix for un_clip localization string
Fixes ReferenceError when copying links from recently uploaded files.
  The un_clip localization string was missing the L. prefix in up2k.js.

  Fixes #467
2025-08-04 18:11:16 +00:00
ed 8c000fd683 cleanup 2025-08-04 00:40:27 +00:00
ed d4397e7217 update pkgs to 1.18.10 2025-08-04 00:39:26 +00:00
ed d7e7e77f93 v1.18.10 2025-08-04 00:13:54 +00:00
ed 715f8424b3 tl cleanup 2025-08-03 23:42:51 +00:00
ed 40d56bb3f0 indicate play-as-audio for video files 2025-08-03 23:35:08 +00:00
ed f9502c3df3 add idp-cookie; for high-traffic / glitchy auth servers 2025-08-03 23:27:53 +00:00
ed ae5eefc528 add sfx explanation; #345 2025-08-03 23:02:56 +00:00
ed 6eaf8af15a pypi: add extras-group "all"; closes #398 2025-08-03 22:50:12 +00:00
ed 848315c009 do not force d2d with default vfs; #295
fixes v1.18.3 regression
2025-08-03 22:43:57 +00:00
ed 47fa4a9299 fix nosub with PUT uploads; closes #412 2025-08-03 22:34:37 +00:00
ed 9db8037e39 remove old joke
end of an era
2025-08-03 22:24:36 +00:00
ed 39e5582496 ignore browser-extension errors; closes #435 2025-08-03 22:14:31 +00:00
ed 16bbcce51b videos can be folder thumbnails; closes #459 2025-08-03 22:10:33 +00:00
ed e85a71070e docs: groups; closes #461 2025-08-03 21:42:26 +00:00
ed d0499257c8 fix tests 2025-08-03 21:36:38 +00:00
ed 7d3a5c1e97 black 2025-08-03 21:35:52 +00:00
ed 153d240d0d mention the -nc option in the max-conn errormsg 2025-08-03 21:35:33 +00:00
ed 66a5bf365b fix ipv6 qrcode port; closes #449 2025-08-03 21:33:08 +00:00
ed 0d09fb6818 audio transcoding tweaks 2025-08-03 21:23:41 +00:00
Toby Kohlhagen b2d48c646f Add flac transcoding option 2025-08-03 20:23:27 +00:00
Toby Kohlhagen b469db3c62 Add wav transcoding option 2025-08-03 20:23:27 +00:00
AOTREVAI a38e6e65d5
Translate to Italian (#458)
* Translating to Italian
* Update browser.js
* sync splash.js, browser.js
* Update browser.js
* Update splash.js

---------

Signed-off-by: AOTREVAI <46420278+AOTREVAI@users.noreply.github.com>
2025-08-03 20:38:16 +02:00
DeStilleGast 3798e19a26
Dutch translation (#426)
* Update browser.js to include Dutch language
* Update splash.js to include some Dutch translations

Signed-off-by: DeStilleGast <3677706+DeStilleGast@users.noreply.github.com>
2025-08-03 18:20:10 +02:00
Beethoven c805c60f40 (scripts/prep.sh) update
i forgot to follow the style woops

Signed-off-by: Beethoven <44652883+Beethoven-n@users.noreply.github.com>
2025-08-03 13:27:27 +00:00
Chinpo Nya c69c7c8ac0 additional instructions for --ah-cli 2025-08-03 13:26:56 +00:00
Techflash 50f1629355 Add test results for Wii Internet Channel
Signed-off-by: Techflash <72118300+techflashYT@users.noreply.github.com>
2025-08-03 13:23:18 +00:00
ed 0bc1b8f715 readme: webdav and opengraph are incompatible by default 2025-08-03 10:22:52 +00:00
icxes a68d5b03f1 fixup finnish translation here and there 2025-08-03 08:43:47 +00:00
ed 971360e914 set config from PRTY_CONFIG; closes #439 2025-08-02 23:24:32 +00:00
Kazi 7e3825f8f5
More verbose help text for TLS certificate flag (#429)
* Clarify TLS key in help text
* More verbose help text

---------

Signed-off-by: Kazi <kzshantonu@users.noreply.github.com>
2025-08-02 14:06:57 +00:00
ed b700072107 update pkgs to 1.18.9 2025-08-01 21:27:28 +00:00
ed ca22cd8853 v1.18.9 2025-08-01 20:56:27 +00:00
ed 09910ba807 fix GHSA-5662-2rj7-f2v6 ;
an unauthenticated user could make the server inaccessible by
accessing the recent-uploads page and using an expensive filter

fixed by making the filter not regex-based,
only supporting bare-minimum anchoring (^foo bar$)
2025-08-01 20:42:49 +00:00
Beethoven 3c6f0b17d0
add Debian packaging via MPR (#385)
* (scripts/prep.sh) prep mpr package at the same time as the arch package
* (contrib/package/makedeb-mpr/) add MPR package

i wanted this on my raspberry pi and i could've done it with docker but
this gives me a systemd service. i haven't actually uploaded this at all

---------

Signed-off-by: Beethoven <44652883+Beethoven-n@users.noreply.github.com>
2025-08-01 22:10:03 +02:00
ed 4fa7be2a48 change "ack" to "continue";
longer text so easier to tap on mobile, and less confusing for
people who aren't network engineers and/or kernel hackers

thx @JanluOfficial for the idea
2025-08-01 19:56:16 +00:00
ed 941761e6e7 tl cleanup 2025-08-01 19:50:38 +00:00
ed c160428810 support tabs in configfiles; closes #400 2025-08-01 18:00:48 +00:00
ed ad23b253dc add --localtime for ui; closes #312 2025-08-01 17:55:34 +00:00
ed d0d2f206a9 log creator of new/blank markdown files too 2025-08-01 17:49:23 +00:00
exci 7ecedb2ce2
add finnish translation (#381)
* reorder translations alphabetically
* R comes before S
* add initial finnish translation
* add splash.js for finnish translation
* add ct_utc translation (fin)
* fix finnish translation problems pointed out in review

---------

Co-authored-by: icxes <icxes@dev.null@need.moe>
2025-08-01 19:46:46 +02:00
ed fee1416cbc redundant 2025-08-01 15:09:10 +00:00
ed 6d6d79fcbc fix upload-abort in shares; closes #347 2025-08-01 15:08:55 +00:00
KevinXuxuxu 9c19753546 [u2c.py] Fix unicode files argument handling for py2.7 2025-08-01 14:39:57 +00:00
Juan Herruzo 4e8b88d8f6 fixed newline structure 2025-08-01 14:33:20 +00:00
Juan Herruzo 1ee89ec21d order spa alphabetically in language selector 2025-08-01 14:33:20 +00:00
Juan Herruzo 9dcb45133b order spa alphabetically 2025-08-01 14:33:20 +00:00
Juan Herruzo 1a5b7d40a8 changed esp to spa 2025-08-01 14:33:20 +00:00
Juan Herruzo 6e35171c88 added splash.js translation 2025-08-01 14:33:20 +00:00
Juan Herruzo af34fbf1a4 fix badly translated keys 2025-08-01 14:33:20 +00:00
Juan Herruzo be729fe557 small tweaks in the localization when looking a it in the gui 2025-08-01 14:33:20 +00:00
Juan Herruzo a1dfd0be33 added spanish draft 2025-08-01 14:33:20 +00:00
Kevin Leutzinger d357ff0d16 Update README.md
Signed-off-by: Kevin Leutzinger <6435727+kleutzinger@users.noreply.github.com>
2025-07-31 23:07:32 +00:00
Kevin Leutzinger e965dc9c74 Add uvx copyparty
Signed-off-by: Kevin Leutzinger <6435727+kleutzinger@users.noreply.github.com>
2025-07-31 23:07:32 +00:00
Toast f401fa7f6c nix: remove space from list separator
The extra speces made copyparty freak out if you tried listening to an ip
address and a unix socket at the same time
2025-07-31 23:05:31 +00:00
Benjamin Bock b69d590176 Improve Python 2 compatibility 2025-07-31 23:05:03 +00:00
ed 1f966bb9d5 devnotes: add tgz to build-from-scratch procedure 2025-07-31 22:57:14 +00:00
ed 3222ba3acd man 2025-07-31 22:56:45 +00:00
ed 0e35f37638 tl cleanup 2025-07-31 22:11:49 +00:00
ed edb5c2bdce
Revert "Added Dutch(NL) translation" (#380)
This reverts commit a2faf4e1e9.
2025-07-31 21:56:06 +00:00
ed 714744f73e
Revert "Added Dutch(NL) Translation to splash.js (#373)" (#379)
This reverts commit 1c86b64a4e.
2025-07-31 21:55:38 +00:00
Bart 1c86b64a4e
Added Dutch(NL) Translation to splash.js (#373) 2025-07-31 18:43:55 +00:00
crypt0rr a2faf4e1e9 Added Dutch(NL) translation 2025-07-31 18:23:50 +00:00
ed b46b5c35e3 tl cleanup 2025-07-31 18:19:47 +00:00
Vlad fea45e451d
add Ukrainian translation (#350) 2025-07-31 19:20:36 +02:00
Altair 0b05c726de
Translate to Russian (#321) 2025-07-31 19:01:41 +02:00
ed cd460902b0 update pkgs to 1.18.8 2025-07-31 08:45:18 +00:00
ed dccef40f3d v1.18.8 2025-07-31 08:33:34 +00:00
ed c17ce4892e fix pkgres on older python3 versions 2025-07-31 08:32:52 +00:00
ed 5df2cbe5d7 update pkgs to 1.18.7 2025-07-30 21:59:58 +00:00
ed daa44be1a5 v1.18.7 2025-07-30 21:31:54 +00:00
ed 13d5631b48 more escapes in case 2025-07-30 21:26:27 +00:00
ed a8705e611d fix GHSA-8mx2-rjh8-q3jq ;
this fixes a DOM-Based XSS in the recent-uploads page:

it was possible to execute arbitrary javascript by
tricking someone into visiting `/?ru&filter=</script>`

huge thanks to @Ju0x for finding and reporting this!
2025-07-30 21:19:39 +00:00
ed b7ca6f4a66 try to fix #300
the importlib stuff broke early versions of py2.7
2025-07-30 21:07:47 +00:00
ed 4f1eb89382 just moving some stuff around, not foreshadowing 2025-07-30 21:05:37 +00:00
Raphael Guntersweiler 9d32564c68
translate to german (#212)
* added german translation
2025-07-30 20:34:51 +00:00
ed 6016ec9388 connectpage: fix sharex 2025-07-30 20:30:18 +00:00
ed fb7cbc423b shares: move all config to webroot 2025-07-30 19:43:47 +00:00
ed e9684d402e fix ipv6 cors-chk 2025-07-30 19:41:45 +00:00
ed 6069bc9b19 mention optional idp persistence 2025-07-30 19:38:33 +00:00
ed f195998865 per-volume uid/gid; closes #265 2025-07-30 19:35:00 +00:00
ed a9d07c63ed disable libmagic on windows; probably closes #276 2025-07-30 18:02:11 +00:00
ed 053de61907 explain what Leeloo Dallas is doing here (closes #316)
also makes rejections from IdP auths less confusing;
it was handled by the config-parser throwing "invalid config"
2025-07-30 17:26:58 +00:00
Jo c3cc2ddeae
diskfree without root-reserved space (#285)
Signed-off-by: Jo <141064017+Arklaum@users.noreply.github.com>
2025-07-29 20:24:17 +00:00
ed 4988a55ea5 webdav: send diskfree; closes #272 2025-07-29 20:07:11 +00:00
ed 5c6341e99f disk-info: both free+total on windows too (#272) 2025-07-29 20:03:42 +00:00
ed fbf17be203 apply unlist to navpane too 2025-07-29 18:14:51 +00:00
ed 3cde1f3be2 docker-compose: PYTHONUNBUFFERED=1
almost zero performance impact with podman in kitty
2025-07-29 17:13:34 +00:00
Tom van Dijk 4915b14be1
various improvements to the nix files (#228)
* nix: allow passing extra packages in PATH

* nix: allow passing extra python packages

I wanted to use
https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py
but that wasn't really possible without this under the nix package.

* nix: format all nix files with nixfmt

* nix: reduce redundancy in the package

For readability

* nix: remove unused pyftpdlib import

* nix: put makeWrapper into the correct inputs

* nix: fill out all of meta

* nix: set formatter in flake for nix files

This allows contributors to format their nix changes with the `nix fmt`
command.

* nix: add u2c

* nix: add partyfuse

One downside of the way the nix ecosystem works is that MacFUSE needs to
be installed manually. Luckily the script tells you that already!

* nix: add missing cfssl import

* nix: add flake check that makes sure it builds with all flags

Because sometimes an import might be missing, and if it is an optional
then you'll only figure out that it's broken if you set the flag.

* nix: use correct overlay argument names

Or `nix flake check` will refuse to run the copyparty-full check
2025-07-29 00:16:30 +00:00
ed 735d9f9391 update pkgs to 1.18.6 2025-07-28 23:45:26 +00:00
ed cd40adccdb v1.18.6 2025-07-28 23:20:07 +00:00
ed 0f2c623599 nosub should prevent mkdir 2025-07-28 23:08:41 +00:00
ed 4adbe1b517 readme: fedora package is happening 2025-07-28 22:36:05 +00:00
ed 4f013f64fe fix helptext typo; closes #244 2025-07-28 22:24:14 +00:00
ed a9d1310296 wait lol 2025-07-28 22:20:50 +00:00
Adam 43e6da3454
add demo video link (#190)
* add feature showcase video

Signed-off-by: Adam <134429563+RustoMCSpit@users.noreply.github.com>

* add youtube link too

Signed-off-by: ed <s@ocv.me>

---------

Signed-off-by: Adam <134429563+RustoMCSpit@users.noreply.github.com>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-07-28 22:19:01 +00:00
AppleTheGolden 542a1de1ba cbz thumbnails: sort alphabetically
Comic readers will sort alphabetically, but that isn't always the order in which the files are stored in the zip.
2025-07-28 22:01:53 +00:00
ed 03d23daecb improve chmod helptext 2025-07-28 20:43:34 +00:00
ed cb019afecf standardize on /dev/shm/party.sock; closes #229 2025-07-28 20:29:40 +00:00
ptweezy 5b98e104f2 Update docker-compose.yml
The version attribute is deprecated, resolves error "the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" when building with Docker

Signed-off-by: ptweezy <parkerbrayden@gmail.com>
2025-07-28 20:10:06 +00:00
ed df9feabcf8 add reflink-based dedup; closes #201 2025-07-28 19:46:15 +00:00
ed 674fc1fe08 make nginx example less confusing 2025-07-28 19:46:15 +00:00
ed a2601fd6ad chpw ratelimit 2025-07-28 19:46:15 +00:00
ed 025942a7d6
connect: hide "use real pw" when no accs (#242)
Disable the "use the real password" button on the connect page when there's no accounts
2025-07-28 19:33:16 +00:00
ed 510100c86b
Update svcs.js
Signed-off-by: ed <s@ocv.me>
2025-07-28 19:31:37 +00:00
Toast 161bbc7d26 connect-page: disable use real password button when there's no accounts 2025-07-28 21:14:26 +02:00
Chinpo Nya 7c9c962b79 nix: add /etc/group to systemd sandbox
allows specifying groups by name in the unix socket
2025-07-28 18:32:55 +00:00
ed cbdbaf1938 update pkgs to 1.18.5 2025-07-27 23:38:32 +00:00
ed cdfceb483e v1.18.5 2025-07-27 23:05:44 +00:00
ed 2228f81f94 block externally-hosted m3u files;
pointless security risk; made GHSA-9q4r-x2hj-jmvr much worse
2025-07-27 22:59:16 +00:00
ed 895880aeb0 fix GHSA-9q4r-x2hj-jmvr ;
this fixes a DOM-Based XSS when rendering multimedia metadata

assuming the media-indexing option is enabled, a malicious media file
could be uploaded to the server by a privileged user, executing
arbitrary javascript on anyone visiting and viewing the directory

the same vulnerability could also be triggered through an
externally-hosted m3u file, by tricking a user into
clicking a link to load and play this m3u file

huge thanks to @altperfect for finding and reporting this!
2025-07-27 22:56:38 +00:00
ed 6bb27e6091 audioplayer: stop at end-of-(song/folder); closes #214 2025-07-27 22:14:16 +00:00
ed d197e754b9 fix scroll after logtail (thx @Bevinsky)
if file was closed without using the [X] button, for example
with the browser back button, the tail would not abort
2025-07-27 21:17:44 +00:00
ed b0dec83aad connect: fix ipv6 and resolve .local only; closes #202 2025-07-27 20:32:45 +00:00
Masked e2c2dd18cf Improve host IP address handling in HttpCli
Added logic to detect if the user provided an IP address or hostname using the ipaddress module. This ensures correct resolution and mapping behavior based on the input type, improving reliability and correctness in network operations.
2025-07-27 19:51:40 +00:00
ed ca6d0b8d5e SameSite=Strict as default; closes #189 2025-07-27 18:18:49 +00:00
ed 48705a74c6 versus: nextcloud does chunked uploads 2025-07-26 18:22:51 +00:00
ed b419984709 docker: add ftps support 2025-07-26 10:50:38 +00:00
ed e00b97eee0 update pkgs to 1.18.4 2025-07-25 18:56:12 +00:00
100 changed files with 16045 additions and 1103 deletions

3
.gitignore vendored
View file

@ -43,3 +43,6 @@ scripts/docker/*.err
# nix build output link # nix build output link
result result
# IDEA config
.idea/

126
README.md
View file

@ -12,7 +12,7 @@ turn almost any device into a file server with resumable uploads/downloads using
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) 📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) 🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) // 👉 **[feature-showcase](https://a.ocv.me/pub/demo/showcase-hq.webm)** ([youtube](https://www.youtube.com/watch?v=15_-hgsX2V0))
made in Norway 🇳🇴 made in Norway 🇳🇴
@ -80,6 +80,7 @@ made in Norway 🇳🇴
* [periodic rescan](#periodic-rescan) - filesystem monitoring * [periodic rescan](#periodic-rescan) - filesystem monitoring
* [upload rules](#upload-rules) - set upload rules using volflags * [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload * [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
* [other flags](#other-flags) * [other flags](#other-flags)
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
@ -89,7 +90,9 @@ made in Norway 🇳🇴
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/)) * [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/)) * [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
* [ip auth](#ip-auth) - autologin based on IP range (CIDR) * [ip auth](#ip-auth) - autologin based on IP range (CIDR)
* [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR)
* [identity providers](#identity-providers) - replace copyparty passwords with oauth and such * [identity providers](#identity-providers) - replace copyparty passwords with oauth and such
* [generic header auth](#generic-header-auth) - other ways to auth by header
* [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords * [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords
* [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar * [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar
* [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed * [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed
@ -146,11 +149,14 @@ made in Norway 🇳🇴
just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉 just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
> the sfx is a [self-extractor](https://github.com/9001/copyparty/issues/270) which unpacks an embedded `tar.gz` into `$TEMP` -- if this looks too scary, you can use the [zipapp](#zipapp) which has slightly worse performance
* or install through [pypi](https://pypi.org/project/copyparty/): `python3 -m pip install --user -U copyparty` * or install through [pypi](https://pypi.org/project/copyparty/): `python3 -m pip install --user -U copyparty`
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead * or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
* or install [on arch](#arch-package) [on NixOS](#nixos-module) [through nix](#nix-package) * or install [on arch](#arch-package) [on NixOS](#nixos-module) [through nix](#nix-package)
* or if you are on android, [install copyparty in termux](#install-on-android) * or if you are on android, [install copyparty in termux](#install-on-android)
* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md) * or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)
* or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty`
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp) * or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
* or if your OS is dead, give the [bootable flashdrive / cd-rom](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) a spin * or if your OS is dead, give the [bootable flashdrive / cd-rom](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) a spin
* or if you don't trust copyparty yet and want to isolate it a little, then... * or if you don't trust copyparty yet and want to isolate it a little, then...
@ -262,6 +268,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ realtime streaming of growing files (logfiles and such) * ☑ realtime streaming of growing files (logfiles and such)
* ☑ [thumbnails](#thumbnails) * ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow, pyvips, or FFmpeg * ☑ ...of images using Pillow, pyvips, or FFmpeg
* ☑ ...of RAW images using rawpy
* ☑ ...of videos using FFmpeg * ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ cache eviction (max-age; maybe max-size eventually)
@ -433,6 +440,7 @@ upgrade notes
* can I link someone to a password-protected volume/file by including the password in the URL? * can I link someone to a password-protected volume/file by including the password in the URL?
* yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end * yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end
* if you have enabled `--usernames` then do `?pw=username:password` instead
* how do I stop `.hist` folders from appearing everywhere on my HDD? * how do I stop `.hist` folders from appearing everywhere on my HDD?
* by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location) * by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location)
@ -507,16 +515,23 @@ examples:
* replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it * replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it
* replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return * replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return
if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct`
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`; 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` 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)
```yaml ```yaml
[accounts] [accounts]
u1: p1 # create account "u1" with password "p1" u1: p1 # create account "u1" with password "p1"
u2: p2 # (note that comments must have u2: p2 # (note that comments must have
u3: p3 # two spaces before the # sign) u3: p3 # two spaces before the # sign)
[groups]
g1: u1, u2 # create a group
[/] # this URL will be mapped to... [/] # this URL will be mapped to...
/srv # ...this folder on the server filesystem /srv # ...this folder on the server filesystem
accs: accs:
@ -526,6 +541,8 @@ and if you want to use config files instead of commandline args (good!) then her
/mnt/music # which is mapped to this folder /mnt/music # which is mapped to this folder
accs: accs:
r: u1, u2 # only these accounts can read, r: u1, u2 # only these accounts can read,
r: @g1 # (exactly the same, just with a group instead)
r: @acct # (alternatively, ALL users who are logged in)
rw: u3 # and only u3 can read-write rw: u3 # and only u3 can read-write
[/inc] [/inc]
@ -1006,6 +1023,7 @@ a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3
url parameters: url parameters:
* `pw=hunter2` for password auth * `pw=hunter2` for password auth
* if you enabled `--usernames` then do `pw=username:password` instead
* `recursive` to also include subfolders * `recursive` to also include subfolders
* `title=foo` changes the feed title (default: folder name) * `title=foo` changes the feed title (default: folder name)
* `fext=mp3,opus` only include mp3 and opus files (default: all) * `fext=mp3,opus` only include mp3 and opus files (default: all)
@ -1082,6 +1100,9 @@ open the `[🎺]` media-player-settings tab to configure it,
* `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1 * `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1
* `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the most part * `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the most part
* `[mp3]` -- the myth, the legend, the undying master of mediocre sound quality that definitely works everywhere * `[mp3]` -- the myth, the legend, the undying master of mediocre sound quality that definitely works everywhere
* `[flac]` -- lossless but compressed, for LAN and/or fiber playback on electrostatic headphones
* `[wav]` -- lossless and uncompressed, for LAN and/or fiber playback on electrostatic headphones connected to very old equipment
* `flac` and `wav` must be enabled with `--allow-flac` / `--allow-wav` to allow spending the disk space
* "tint" reduces the contrast of the playback bar * "tint" reduces the contrast of the playback bar
@ -1216,7 +1237,7 @@ using arguments or config files, or a mix of both:
**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags. **NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help` * if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
* or see this (probably outdated): https://ocv.me/copyparty/helptext.html * or see this: https://ocv.me/copyparty/helptext.html
* or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt * or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt
@ -1288,6 +1309,7 @@ an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit T
* if you enable both `ftp` and `ftps`, the port-range will be divided in half * if you enable both `ftp` and `ftps`, the port-range will be divided in half
* some older software (filezilla on debian-stable) cannot passive-mode with TLS * some older software (filezilla on debian-stable) cannot passive-mode with TLS
* login with any username + your password, or put your password in the username field * login with any username + your password, or put your password in the username field
* unless you enabled `--usernames`
some recommended FTP / FTPS clients; `wark` = example password: some recommended FTP / FTPS clients; `wark` = example password:
* https://winscp.net/eng/download.php * https://winscp.net/eng/download.php
@ -1305,6 +1327,7 @@ click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to se
general usage: general usage:
* login with any username + your password, or put your password in the username field (password field can be empty/whatever) * login with any username + your password, or put your password in the username field (password field can be empty/whatever)
* unless you enabled `--usernames`
on macos, connect from finder: on macos, connect from finder:
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/ * [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
@ -1320,6 +1343,7 @@ using the GUI (winXP or later):
* rightclick [my computer] -> [map network drive] -> Folder: `http://192.168.123.1:3923/` * rightclick [my computer] -> [map network drive] -> Folder: `http://192.168.123.1:3923/`
* on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there * on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there
* providing your password as the username is recommended; the password field can be anything or empty * providing your password as the username is recommended; the password field can be anything or empty
* unless you enabled `--usernames`
the webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead: the webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead:
* win7+ doesn't actually send the password to the server when reauthenticating after a reboot unless you first try to login with an incorrect password and then switch to the correct password * win7+ doesn't actually send the password to the server when reauthenticating after a reboot unless you first try to login with an incorrect password and then switch to the correct password
@ -1377,6 +1401,7 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
* the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh) or [bubbleparty](./bin/bubbleparty.sh) * the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh) or [bubbleparty](./bin/bubbleparty.sh)
* account passwords work per-volume as expected, and so does account permissions (read/write/move/delete), but `--smbw` must be given to allow write-access from smb * account passwords work per-volume as expected, and so does account permissions (read/write/move/delete), but `--smbw` must be given to allow write-access from smb
* [shadowing](#shadowing) probably works as expected but no guarantees * [shadowing](#shadowing) probably works as expected but no guarantees
* not compatible with pw-hashing or `--usernames`
and some minor issues, and some minor issues,
* clients only see the first ~400 files in big folders; * clients only see the first ~400 files in big folders;
@ -1423,6 +1448,8 @@ note that this disables hotlinking because the opengraph spec demands it; to sne
you can also hotlink files regardless by appending `?raw` to the url you can also hotlink files regardless by appending `?raw` to the url
> WARNING: if you plan to use WebDAV, then `--og-ua` / `og_ua` must be configured
if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object) if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object)
@ -1439,12 +1466,17 @@ if you enable deduplication with `--dedup` then it'll create a symlink instead o
**warning:** when enabling dedup, you should also: **warning:** when enabling dedup, you should also:
* enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended * enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended
* ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below * ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below
* ...and/or `--reflink` to use CoW/reflink-based dedup (much safer than hardlink, but OS/FS-dependent)
it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to) it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to)
by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`; you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`, and you can choose to use reflinks with `--reflink` or volflag `reflink`
advantages of using reflinks (CoW, copy-on-write):
* entirely safe (when your filesystem supports it correctly); either file can be edited or deleted without affecting other copies
* only linux 5.3 or newer, only python 3.14 or newer, only some filesystems (btrfs probably ok, maybe xfs too, but zfs had bugs)
advantages of using hardlinks: advantages of using hardlinks:
* hardlinks are more compatible with other software; they behave entirely like regular files * hardlinks are more compatible with other software; they behave entirely like regular files
@ -1644,6 +1676,26 @@ some examples,
allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4` allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4`
## chmod and chown
per-volume filesystem-permissions and ownership
by default:
* all folders are chmod 755
* files are usually chmod 644 (umask-defined)
* user/group is whatever copyparty is running as
this can be configured per-volume:
* volflag `chmod_f` sets file permissions; default=`644` (usually)
* volflag `chmod_d` sets directory permissions; default=`755`
* volflag `uid` sets the owner user-id
* volflag `gid` sets the owner group-id
notes:
* `gid` can only be set to one of the groups which the copyparty process is a member of
* `uid` can only be set if copyparty is running as root (i appreciate your faith)
## other flags ## other flags
* `:c,magic` enables filetype detection for nameless uploads, same as `--magic` * `:c,magic` enables filetype detection for nameless uploads, same as `--magic`
@ -1846,6 +1898,20 @@ repeat the option to map additional subnets
**be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=` **be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=`
### restrict to ip
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:
```yaml
[global]
ipr: 192.168.123.0/24,172.16.0.0/16=spartacus
```
repeat the option to map additional users
## identity providers ## identity providers
replace copyparty passwords with oauth and such replace copyparty passwords with oauth and such
@ -1854,6 +1920,8 @@ you can disable the built-in password-based login system, and instead replace it
* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
* if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type) there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type)
@ -1863,6 +1931,20 @@ a more complete example of the copyparty configuration options [look like this](
but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead
### generic header auth
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:
```yaml
[global]
idp-hm-usr: ^Tailscale-User-Login^alice.m@forest.net^alice
```
repeat the whole `idp-hm-usr` option to add more mappings
## user-changeable passwords ## user-changeable passwords
if permitted, users can change their own passwords in the control-panel if permitted, users can change their own passwords in the control-panel
@ -2016,13 +2098,17 @@ you can either:
* or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs * or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs
* if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below * if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below
when running behind a reverse-proxy (this includes services like cloudflare), it is important to configure real-ip correctly, as many features rely on knowing the client's IP. Look out for red and yellow log messages which explain how to do this. But basically, set `--xff-hdr` to the name of the http header to read the IP from (usually `x-forwarded-for`, but cloudflare uses `cf-connecting-ip`), and then `--xff-src` to the IP of the reverse-proxy so copyparty will trust the xff-hdr. Note that `--rp-loc` in particular will not work at all unless you do this when running behind a reverse-proxy (this includes services like cloudflare), it is important to configure real-ip correctly, as many features rely on knowing the client's IP. The best/safest approach is to configure your reverse-proxy so it gives copyparty a header which only contains the client's true/real IP-address, and then setting `--xff-hdr theHeaderName --rproxy 1` but alternatively, if you want/need to let copyparty handle this, look out for red and yellow log messages which explain how to do that. Basically, the log will say this:
> set `--xff-hdr` to the name of the http-header to read the IP from (usually `x-forwarded-for`, but cloudflare uses `cf-connecting-ip`), and then `--xff-src` to the IP of the reverse-proxy so copyparty will trust the xff-hdr. You will also need to configure `--rproxy` to `1` if the header only contains one IP (the correct one) or to a *negative value* if it contains multiple; `-1` being the rightmost and most trusted IP (the nearest proxy, so usually not the correct one), `-2` being the second-closest hop, and so on
Note that `--rp-loc` in particular will not work at all unless you configure the above correctly
some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which *could* be a nice speed boost, depending on a lot of factors some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which *could* be a nice speed boost, depending on a lot of factors
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now * **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2 * depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/tmp/party.sock` (permission `770` means only members of group `www` can access it) for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/dev/shm/party.sock` (permission `770` means only members of group `www` can access it)
example webserver / reverse-proxy configs: example webserver / reverse-proxy configs:
@ -2221,6 +2307,7 @@ force-enable features with known issues on your OS/env by setting any of the fo
| env-var | what it does | | env-var | what it does |
| ------------------------ | ------------ | | ------------------------ | ------------ |
| `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms | | `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
| `PRTY_FORCE_MAGIC` | use [magic](https://pypi.org/project/python-magic/) on Windows (you will segfault) |
# packages # packages
@ -2234,16 +2321,14 @@ 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/)) `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
it comes with a [systemd service](./contrib/package/arch/copyparty.service) and expects to find one or more [config files](./docs/example.conf) in `/etc/copyparty.d/` 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`
after installing it, you may want to `cp /usr/lib/systemd/system/copyparty.service /etc/systemd/system/` and then `vim /etc/systemd/system/copyparty.service` to change what user/group it is running as (you only need to do this once) 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)
NOTE: there used to be an aur package; this evaporated when copyparty was adopted by the official archlinux repos. If you're still using the aur package, please move
## fedora package ## fedora package
does not exist yet; using the [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) builds is **NOT recommended** because updates can be delayed by [several months](https://github.com/fedora-copr/copr/issues/3056) does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
## nix package ## nix package
@ -2401,6 +2486,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl | | **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
| **sony psp** 5.50 | can browse, upload/mkdir/msg (thx dwarf) [screenshot](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) | | **sony psp** 5.50 | can browse, upload/mkdir/msg (thx dwarf) [screenshot](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) |
| **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) | | **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) |
| **Nintendo Wii (Opera 9.0 "Internet Channel")** | can browse, can't upload or download (no local storage), can view images - works best with `?b=u`, default view broken |
<p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p> <p align="center"><img src="https://github.com/user-attachments/assets/88deab3d-6cad-4017-8841-2f041472b853" /></p>
@ -2460,6 +2546,8 @@ you can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, ur
> for basic-authentication, all of the following are accepted: `password` / `whatever:password` / `password:whatever` (the username is ignored) > for basic-authentication, all of the following are accepted: `password` / `whatever:password` / `password:whatever` (the username is ignored)
* unless you've enabled `--usernames`, then it's `PW: usr:pwd`, cookie `cppwd=usr:pwd`, url-param `?pw=usr:pwd`
NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename
@ -2571,7 +2659,7 @@ there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone`
some notes on hardening some notes on hardening
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy) * set `--rproxy 0` *if and only if* your copyparty is directly facing the internet (not through a reverse-proxy)
* cors doesn't work right otherwise * cors doesn't work right otherwise
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml` * if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
* this returns html documents as plaintext, and also disables markdown rendering * this returns html documents as plaintext, and also disables markdown rendering
@ -2671,6 +2759,12 @@ optionally also specify `--ah-cli` to enter an interactive mode where it will ha
the default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop the default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop
when generating hashes using `--ah-cli` for docker or systemd services, make sure it is using the same `--ah-salt` by:
* inspecting the generated salt using `--show-ah-salt` in copyparty service configuration
* setting the same `--ah-salt` in both environments
> ⚠️ if you have enabled `--usernames` then provide the password as `username:password` when hashing it, for example `ed:hunter2`
## https ## https
@ -2732,9 +2826,10 @@ enable [music tags](#metadata-from-audio-files):
enable [thumbnails](#thumbnails) of... enable [thumbnails](#thumbnails) of...
* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+) * **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH` * **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler) * **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+ * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
* **JPEG XL pictures:** `pyvips` or `ffmpeg` * **JPEG XL pictures:** `pyvips` or `ffmpeg`
* **RAW images:** `rawpy`, plus one of `pyvips` or `Pillow` (for some formats)
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq` enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
@ -2765,9 +2860,10 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg | | `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails | | `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
| `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) | | `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) |
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pyheif-pillow-opener/) | | `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) |
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow | | `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows | | `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
| `PRTY_NO_RAW` | disable all [rawpy](https://pypi.org/project/rawpy/)-based thumbnail support for RAW images |
| `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg | | `PRTY_NO_VIPS` | disable all [libvips](https://pypi.org/project/pyvips/)-based thumbnail support; will fallback to Pillow or ffmpeg |
example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py` example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
@ -2788,6 +2884,8 @@ these are standalone programs and will never be imported / evaluated by copypart
the self-contained "binary" (recommended!) [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course the self-contained "binary" (recommended!) [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
if you only need english, [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py) is the same thing but smaller
you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack) you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)
@ -2815,7 +2913,7 @@ then again, if you are already into downloading shady binaries from the internet
## zipapp ## zipapp
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, is slow, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it does not unpack any temporary files to disk, so it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, is slow, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but, unlike the sfx, it is a completely normal zipfile which does not unpack any temporary files to disk, so it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails

View file

@ -52,6 +52,7 @@ if PY2:
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
bytes = str bytes = str
files_decoder = lambda s: unicode(s, "utf8")
else: else:
from urllib.parse import quote_from_bytes as quote from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote from urllib.parse import unquote_to_bytes as unquote
@ -61,6 +62,7 @@ else:
from queue import Queue from queue import Queue
unicode = str unicode = str
files_decoder = unicode
WTF8 = "replace" if PY2 else "surrogateescape" WTF8 = "replace" if PY2 else "surrogateescape"
@ -1532,7 +1534,7 @@ source file/folder selection uses rsync syntax, meaning that:
""") """)
ap.add_argument("url", type=unicode, help="server url, including destination folder") ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process") 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("-v", action="store_true", help="verbose")
ap.add_argument("-a", metavar="PASSWD", help="password or $filepath") ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)") ap.add_argument("-s", action="store_true", help="file-search (disables upload)")

View file

@ -85,13 +85,13 @@ server {
proxy_buffer_size 16k; proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; proxy_busy_buffers_size 24k;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# NOTE: with cloudflare you want this instead:
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "Keep-Alive"; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# NOTE: with cloudflare you want this X-Forwarded-For instead:
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
} }
} }

View file

@ -4,28 +4,31 @@
lib, lib,
... ...
}: }:
with lib; let with lib;
mkKeyValue = key: value: let
if value == true mkKeyValue =
then key: value:
if value == true then
# sets with a true boolean value are coerced to just the key name # sets with a true boolean value are coerced to just the key name
key key
else if value == false else if value == false then
then
# or omitted completely when false # or omitted completely when false
"" ""
else (generators.mkKeyValueDefault {inherit mkValueString;} ": " key value); else
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
mkAttrsString = value: (generators.toKeyValue {inherit mkKeyValue;} value); mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
mkValueString = value: mkValueString =
if isList value value:
then (concatStringsSep ", " (map mkValueString value)) if isList value then
else if isAttrs value (concatStringsSep "," (map mkValueString value))
then "\n" + (mkAttrsString value) else if isAttrs value then
else (generators.mkValueStringDefault {} value); "\n" + (mkAttrsString value)
else
(generators.mkValueStringDefault { } value);
mkSectionName = value: "[" + (escape ["[" "]"] value) + "]"; mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]";
mkSection = name: attrs: '' mkSection = name: attrs: ''
${mkSectionName name} ${mkSectionName name}
@ -57,7 +60,8 @@ with lib; let
externalCacheDir = "/var/cache/copyparty"; externalCacheDir = "/var/cache/copyparty";
externalStateDir = "/var/lib/copyparty"; externalStateDir = "/var/lib/copyparty";
defaultShareDir = "${externalStateDir}/data"; defaultShareDir = "${externalStateDir}/data";
in { in
{
options.services.copyparty = { options.services.copyparty = {
enable = mkEnableOption "web-based file manager"; enable = mkEnableOption "web-based file manager";
@ -128,22 +132,27 @@ in {
}; };
accounts = mkOption { accounts = mkOption {
type = types.attrsOf (types.submodule ({...}: { type = types.attrsOf (
options = { types.submodule (
passwordFile = mkOption { { ... }:
type = types.str; {
description = '' options = {
Runtime file path to a file containing the user password. passwordFile = mkOption {
Must be readable by the copyparty user. type = types.str;
''; description = ''
example = "/run/keys/copyparty/ed"; Runtime file path to a file containing the user password.
}; Must be readable by the copyparty user.
}; '';
})); example = "/run/keys/copyparty/ed";
};
};
}
)
);
description = '' description = ''
A set of copyparty accounts to create. A set of copyparty accounts to create.
''; '';
default = {}; default = { };
example = literalExpression '' example = literalExpression ''
{ {
ed.passwordFile = "/run/keys/copyparty/ed"; ed.passwordFile = "/run/keys/copyparty/ed";
@ -152,74 +161,81 @@ in {
}; };
volumes = mkOption { volumes = mkOption {
type = types.attrsOf (types.submodule ({...}: { type = types.attrsOf (
options = { types.submodule (
path = mkOption { { ... }:
type = types.path; {
description = '' options = {
Path of a directory to share. path = mkOption {
''; type = types.path;
}; description = ''
access = mkOption { Path of a directory to share.
type = types.attrs; '';
description = ''
Attribute list of permissions and the users to apply them to.
The key must be a string containing any combination of allowed permission:
"r" (read): list folder contents, download files
"w" (write): upload files; need "r" to see the uploads
"m" (move): move files and folders; need "w" at destination
"d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads
"h" (html): "get", but folders return their index.html
"a" (admin): can see uploader IPs, config-reload
For example: "rwmd"
The value must be one of:
an account name, defined in `accounts`
a list of account names
"*", which means "any account"
'';
example = literalExpression ''
{
# wG = write-upget = see your own uploads only
wG = "*";
# read-write-modify-delete for users "ed" and "k"
rwmd = ["ed" "k"];
}; };
''; access = mkOption {
}; type = types.attrs;
flags = mkOption { description = ''
type = types.attrs; Attribute list of permissions and the users to apply them to.
description = ''
Attribute list of volume flags to apply. The key must be a string containing any combination of allowed permission:
See `${getExe cfg.package} --help-flags` for more details. "r" (read): list folder contents, download files
''; "w" (write): upload files; need "r" to see the uploads
example = literalExpression '' "m" (move): move files and folders; need "w" at destination
{ "d" (delete): permanently delete files and folders
# "fk" enables filekeys (necessary for upget permission) (4 chars long) "g" (get): download files, but cannot see folder contents
fk = 4; "G" (upget): "get", but can see filekeys of their own uploads
# scan for new files every 60sec "h" (html): "get", but folders return their index.html
scan = 60; "a" (admin): can see uploader IPs, config-reload
# volflag "e2d" enables the uploads database
e2d = true; For example: "rwmd"
# "d2t" disables multimedia parsers (in case the uploads are malicious)
d2t = true; The value must be one of:
# skips hashing file contents if path matches *.iso an account name, defined in `accounts`
nohash = "\.iso$"; a list of account names
"*", which means "any account"
'';
example = literalExpression ''
{
# wG = write-upget = see your own uploads only
wG = "*";
# read-write-modify-delete for users "ed" and "k"
rwmd = ["ed" "k"];
};
'';
}; };
''; flags = mkOption {
default = {}; type = types.attrs;
}; description = ''
}; Attribute list of volume flags to apply.
})); See `${getExe cfg.package} --help-flags` for more details.
'';
example = literalExpression ''
{
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
fk = 4;
# scan for new files every 60sec
scan = 60;
# volflag "e2d" enables the uploads database
e2d = true;
# "d2t" disables multimedia parsers (in case the uploads are malicious)
d2t = true;
# skips hashing file contents if path matches *.iso
nohash = "\.iso$";
};
'';
default = { };
};
};
}
)
);
description = "A set of copyparty volumes to create"; description = "A set of copyparty volumes to create";
default = { default = {
"/" = { "/" = {
path = defaultShareDir; path = defaultShareDir;
access = {r = "*";}; access = {
r = "*";
};
}; };
}; };
example = literalExpression '' example = literalExpression ''
@ -238,92 +254,90 @@ in {
}; };
}; };
config = mkIf cfg.enable (let config = mkIf cfg.enable (
command = "${getExe cfg.package} -c ${runtimeConfigPath}"; let
in { command = "${getExe cfg.package} -c ${runtimeConfigPath}";
systemd.services.copyparty = { in
description = "http file sharing hub"; {
wantedBy = ["multi-user.target"]; systemd.services.copyparty = {
description = "http file sharing hub";
wantedBy = [ "multi-user.target" ];
environment = { environment = {
PYTHONUNBUFFERED = "true"; PYTHONUNBUFFERED = "true";
XDG_CONFIG_HOME = externalStateDir; XDG_CONFIG_HOME = externalStateDir;
}; };
preStart = let preStart =
replaceSecretCommand = name: attrs: "${getExe pkgs.replace-secret} '${ let
passwordPlaceholder name replaceSecretCommand =
}' '${attrs.passwordFile}' ${runtimeConfigPath}"; name: attrs:
in '' "${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}";
set -euo pipefail in
install -m 600 ${configFile} ${runtimeConfigPath} ''
${concatStringsSep "\n" set -euo pipefail
(mapAttrsToList replaceSecretCommand cfg.accounts)} install -m 600 ${configFile} ${runtimeConfigPath}
''; ${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)}
'';
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
ExecStart = command; ExecStart = command;
# Hardening options # Hardening options
User = cfg.user; User = cfg.user;
Group = cfg.group; Group = cfg.group;
RuntimeDirectory = ["copyparty"]; RuntimeDirectory = [ "copyparty" ];
RuntimeDirectoryMode = "0700"; RuntimeDirectoryMode = "0700";
StateDirectory = ["copyparty"]; StateDirectory = [ "copyparty" ];
StateDirectoryMode = "0700"; StateDirectoryMode = "0700";
CacheDirectory = lib.mkIf (cfg.settings ? hist) ["copyparty"]; CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ];
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700"; CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
WorkingDirectory = externalStateDir; WorkingDirectory = externalStateDir;
BindReadOnlyPaths = BindReadOnlyPaths = [
[
"/nix/store" "/nix/store"
"-/etc/resolv.conf" "-/etc/resolv.conf"
"-/etc/nsswitch.conf" "-/etc/nsswitch.conf"
"-/etc/group"
"-/etc/hosts" "-/etc/hosts"
"-/etc/localtime" "-/etc/localtime"
] ] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts); BindPaths =
BindPaths = (if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
( ++ [ externalStateDir ]
if cfg.settings ? hist ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
then [cfg.settings.hist] # ProtectSystem = "strict";
else [] # Note that unlike what 'ro' implies,
) # this actually makes it impossible to read anything in the root FS,
++ [externalStateDir] # except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
++ (mapAttrsToList (k: v: v.path) cfg.volumes); # This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
# ProtectSystem = "strict"; TemporaryFileSystem = "/:ro";
# Note that unlike what 'ro' implies, PrivateTmp = true;
# this actually makes it impossible to read anything in the root FS, PrivateDevices = true;
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`. ProtectKernelTunables = true;
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible. ProtectControlGroups = true;
TemporaryFileSystem = "/:ro"; RestrictSUIDSGID = true;
PrivateTmp = true; PrivateMounts = true;
PrivateDevices = true; ProtectKernelModules = true;
ProtectKernelTunables = true; ProtectKernelLogs = true;
ProtectControlGroups = true; ProtectHostname = true;
RestrictSUIDSGID = true; ProtectClock = true;
PrivateMounts = true; ProtectProc = "invisible";
ProtectKernelModules = true; ProcSubset = "pid";
ProtectKernelLogs = true; RestrictNamespaces = true;
ProtectHostname = true; RemoveIPC = true;
ProtectClock = true; UMask = "0077";
ProtectProc = "invisible"; LimitNOFILE = cfg.openFilesLimit;
ProcSubset = "pid"; NoNewPrivileges = true;
RestrictNamespaces = true; LockPersonality = true;
RemoveIPC = true; RestrictRealtime = true;
UMask = "0077"; MemoryDenyWriteExecute = true;
LimitNOFILE = cfg.openFilesLimit; };
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
}; };
};
# ensure volumes exist: # ensure volumes exist:
systemd.tmpfiles.settings."copyparty" = ( systemd.tmpfiles.settings."copyparty" = (
lib.attrsets.mapAttrs' ( lib.attrsets.mapAttrs' (
name: value: name: value:
lib.attrsets.nameValuePair (value.path) { lib.attrsets.nameValuePair (value.path) {
d = { d = {
#: in front of things means it wont change it if the directory already exists. #: in front of things means it wont change it if the directory already exists.
@ -332,32 +346,30 @@ in {
mode = ":755"; mode = ":755";
}; };
} }
) ) cfg.volumes
cfg.volumes );
);
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {}; users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
description = "Service user for copyparty"; description = "Service user for copyparty";
group = "copyparty"; group = "copyparty";
home = externalStateDir; home = externalStateDir;
isSystemUser = true; isSystemUser = true;
}; };
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [ environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
(pkgs.writeShellScriptBin (pkgs.writeShellScriptBin "copyparty-hash" ''
"copyparty-hash" set -a # automatically export variables
'' # set same environment variables as the systemd service
set -a # automatically export variables ${lib.pipe config.systemd.services.copyparty.environment [
# set same environment variables as the systemd service (lib.filterAttrs (n: v: v != null && n != "PATH"))
${lib.pipe config.systemd.services.copyparty.environment [ (lib.mapAttrs (_: v: "${v}"))
(lib.filterAttrs (n: v: v != null && n != "PATH")) (lib.toShellVars)
(lib.mapAttrs (_: v: "${v}")) ]}
(lib.toShellVars) PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
]}
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
exec ${command} --ah-cli exec ${command} --ah-cli
'') '')
]; ];
}); }
);
} }

View file

@ -1,57 +1,48 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
# Contributor: Morgan Adamiec <morganamilo@archlinux.org>
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty pkgname=copyparty
pkgver="1.18.3" pkgver="1.19.2"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
url="https://github.com/9001/${pkgname}" url="https://github.com/9001/${pkgname}"
license=('MIT') license=('MIT')
depends=("python" "lsof" "python-jinja") depends=("bash" "python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz") makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags" optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"cfssl: generate TLS certificates on startup (pointless when reverse-proxied)" "cfssl: generate TLS certificates on startup"
"python-mutagen: music tags (alternative)" "python-mutagen: music tags (alternative)"
"python-pillow: thumbnails for images" "python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)" "python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
"libkeyfinder-git: detection of musical keys" "libkeyfinder: detection of musical keys"
"qm-vamp-plugins: BPM detection" "python-pyopenssl: ftps functionality"
"python-pyopenssl: ftps functionality" "python-pyzmq: send zeromq messages from event-hooks"
"python-pyzmq: send zeromq messages from event-hooks" "python-argon2-cffi: hashed passwords in config"
"python-argon2-cffi: hashed passwords in config"
"python-impacket-git: smb support (bad idea)"
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" ) backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("aa12f4779cf5c014cc9503798ac63872dac840ca91ddf122daa6befb4c883d48") sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
pushd copyparty/web
make -j$(nproc)
rm Makefile
popd
python3 -m build -wn
} }
package() { package() {
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"
python3 -m installer -d "$pkgdir" dist/*.whl python -m installer --destdir="$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}.d" install -dm755 "${pkgdir}/etc/${pkgname}"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty" install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init" install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service" install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service" install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md" 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 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return
echo "┏━━━━━━━━━━━━━━━──-"
echo "┃ Configure ${pkgname} by adding .conf files into /etc/${pkgname}.d/"
echo "┃ and maybe copy+edit one of the following to /etc/systemd/system/:"
echo "┣━♦ /usr/lib/systemd/system/${pkgname}.service (standard)"
echo "┣━♦ /usr/lib/systemd/system/prisonparty.service (chroot)"
echo "┗━━━━━━━━━━━━━━━──-"
} }

View file

@ -0,0 +1,44 @@
# Contributor: Beethoven <beethovenisadog@protonmail.com>
pkgname=copyparty
pkgver=1.19.2
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
url="https://github.com/9001/${pkgname}"
license=('MIT')
depends=("bash" "python3" "lsof" "python3-jinja2")
makedepends=("python3-wheel" "python3-setuptools" "python3-build" "python3-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"golang-cfssl: generate TLS certificates on startup"
"python3-mutagen: music tags (alternative)"
"python3-pil: thumbnails for images"
"python3-openssl: ftps functionality"
"python3-zmq: send zeromq messages from event-hooks"
"python3-argon2: hashed passwords in config"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("/etc/${pkgname}.d/init" )
sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
python -m build --wheel --no-isolation
}
package() {
cd "${srcdir}/${pkgname}-${pkgver}"
python -m installer --destdir="$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}.d"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
install -Dm644 "contrib/package/makedeb-mpr/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
install -Dm644 "contrib/package/makedeb-mpr/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}

View file

@ -1,45 +1,93 @@
{ lib, stdenv, makeWrapper, fetchurl, util-linux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, pyzmq, ffmpeg, mutagen, {
lib,
buildPythonApplication,
fetchurl,
util-linux,
python,
setuptools,
jinja2,
impacket,
pyopenssl,
cfssl,
argon2-cffi,
pillow,
pyvips,
pyzmq,
ffmpeg,
mutagen,
pyftpdlib,
magic,
partftpy,
fusepy, # for partyfuse
# use argon2id-hashed passwords in config files (sha2 is always available) # use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true, withHashedPasswords ? true,
# generate TLS certificates on startup (pointless when reverse-proxied) # generate TLS certificates on startup (pointless when reverse-proxied)
withCertgen ? false, withCertgen ? false,
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withThumbnails ? true, withThumbnails ? true,
# create thumbnails with PyVIPS; even faster, uses more memory # create thumbnails with PyVIPS; even faster, uses more memory
# -- can be combined with Pillow to support more filetypes # -- can be combined with Pillow to support more filetypes
withFastThumbnails ? false, withFastThumbnails ? false,
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus # enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface # -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both # -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
withMediaProcessing ? true, withMediaProcessing ? true,
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster) # if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
withBasicAudioMetadata ? false, withBasicAudioMetadata ? false,
# send ZeroMQ messages from event-hooks # send ZeroMQ messages from event-hooks
withZeroMQ ? true, withZeroMQ ? true,
# enable FTPS support in the FTP server # enable FTP server
withFTPS ? false, withFTP ? true,
# samba/cifs server; dangerous and buggy, enable if you really need it # enable FTPS support in the FTP server
withSMB ? false, withFTPS ? false,
# enable TFTP server
withTFTP ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# enables filetype detection for nameless uploads
withMagic ? false,
# extra packages to add to the PATH
extraPackages ? [ ],
# function that accepts a python packageset and returns a list of packages to
# be added to the python venv. useful for scripts and such that require
# additional dependencies
extraPythonPackages ? (_p: [ ]),
}: }:
let let
pinData = lib.importJSON ./pin.json; pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (ps: runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
with ps; [ in
buildPythonApplication {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
dependencies =
[
jinja2 jinja2
fusepy
] ]
++ lib.optional withSMB impacket ++ lib.optional withSMB impacket
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl ++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl ++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow ++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips ++ lib.optional withFastThumbnails pyvips
@ -47,22 +95,24 @@ let
++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi ++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq ++ lib.optional withZeroMQ pyzmq
); ++ lib.optional withMagic magic
in stdenv.mkDerivation { ++ (extraPythonPackages python.pkgs);
pname = "copyparty"; makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
version = pinData.version;
src = fetchurl { pyproject = true;
url = pinData.url; build-system = [
hash = pinData.hash; setuptools
];
meta = {
description = "Turn almost any device into a file server";
longDescription = ''
Portable file server with accelerated resumable uploads, dedup, WebDAV,
FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
'';
homepage = "https://github.com/9001/copyparty";
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
license = lib.licenses.mit;
mainProgram = "copyparty";
sourceProvenance = [ lib.sourceTypes.fromSource ];
}; };
buildInputs = [ makeWrapper ];
dontUnpack = true;
dontBuild = true;
installPhase = ''
install -Dm755 $src $out/share/copyparty-sfx.py
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
--set PATH '${lib.makeBinPath ([ util-linux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
--add-flags "$out/share/copyparty-sfx.py"
'';
meta.mainProgram = "copyparty";
} }

View file

@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.18.3/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.19.2/copyparty-1.19.2.tar.gz",
"version": "1.18.3", "version": "1.19.2",
"hash": "sha256-INqErls4gyhBAlDlY1vfNboKrrqHmeiyB+RAuuYRISQ=" "hash": "sha256-nw3NgSTyYKDHJna3DYTII4jP5bR+fQVW9RkMiCCFgKI="
} }

View file

@ -11,14 +11,14 @@ import base64
import json import json
import hashlib import hashlib
import sys import sys
import re import tarfile
from pathlib import Path from pathlib import Path
OUTPUT_FILE = Path("pin.json") OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = "copyparty-sfx.py" TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
HASH_TYPE = "sha256" HASH_TYPE = "sha256"
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest" LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest"
DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}" DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}"
def get_formatted_hash(binary): def get_formatted_hash(binary):
@ -29,11 +29,13 @@ def get_formatted_hash(binary):
return f"{HASH_TYPE}-{encoded_hash}" return f"{HASH_TYPE}-{encoded_hash}"
def version_from_sfx(binary): def version_from_tar_gz(path):
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE) with tarfile.open(path) as tarball:
if result: release_name = tarball.getmembers()[0].name
return result.groups(1)[0].decode("ascii") prefix = "copyparty-"
if release_name.startswith(prefix):
return release_name.replace(prefix, "")
raise ValueError("version not found in provided file") raise ValueError("version not found in provided file")
@ -42,7 +44,7 @@ def remote_release_pin():
response = requests.get(LATEST_RELEASE_URL).json() response = requests.get(LATEST_RELEASE_URL).json()
version = response["tag_name"].lstrip("v") version = response["tag_name"].lstrip("v")
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0] asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
download_url = asset_info["browser_download_url"] download_url = asset_info["browser_download_url"]
asset = requests.get(download_url) asset = requests.get(download_url)
formatted_hash = get_formatted_hash(asset.content) formatted_hash = get_formatted_hash(asset.content)
@ -52,10 +54,9 @@ def remote_release_pin():
def local_release_pin(path): def local_release_pin(path):
asset = path.read_bytes() version = version_from_tar_gz(path)
version = version_from_sfx(asset)
download_url = DOWNLOAD_URL(version) download_url = DOWNLOAD_URL(version)
formatted_hash = get_formatted_hash(asset) formatted_hash = get_formatted_hash(path.read_bytes())
result = {"url": download_url, "version": version, "hash": formatted_hash} result = {"url": download_url, "version": version, "hash": formatted_hash}
return result return result

View file

@ -0,0 +1,30 @@
{
lib,
buildPythonPackage,
fetchurl,
setuptools,
}:
let
pinData = lib.importJSON ./pin.json;
in
buildPythonPackage rec {
pname = "partftpy";
inherit (pinData) version;
pyproject = true;
src = fetchurl {
inherit (pinData) url hash;
};
build-system = [ setuptools ];
pythonImportsCheck = [ "partftpy.TftpServer" ];
meta = {
description = "Pure Python TFTP library (copyparty edition)";
homepage = "https://github.com/9001/partftpy";
changelog = "https://github.com/9001/partftpy/releases/tag/${version}";
license = lib.licenses.mit;
};
}

View file

@ -0,0 +1,5 @@
{
"url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz",
"version": "0.4.0",
"hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0="
}

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# Update the Nix package pin
#
# Usage: ./update.sh
import base64
import json
import hashlib
import sys
from pathlib import Path
OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz"
HASH_TYPE = "sha256"
LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest"
def get_formatted_hash(binary):
hasher = hashlib.new("sha256")
hasher.update(binary)
asset_hash = hasher.digest()
encoded_hash = base64.b64encode(asset_hash).decode("ascii")
return f"{HASH_TYPE}-{encoded_hash}"
def remote_release_pin():
import requests
response = requests.get(LATEST_RELEASE_URL).json()
version = response["tag_name"].lstrip("v")
asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0]
download_url = asset_info["browser_download_url"]
asset = requests.get(download_url)
formatted_hash = get_formatted_hash(asset.content)
result = {"url": download_url, "version": version, "hash": formatted_hash}
return result
def main():
result = remote_release_pin()
print(result)
json_result = json.dumps(result, indent=4)
OUTPUT_FILE.write_text(json_result)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,62 @@
Name: copyparty
Version: $pkgver
Release: $pkgrel
License: MIT
Group: Utilities
URL: https://github.com/9001/copyparty
Source0: copyparty-$pkgver.tar.gz
Summary: File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++
BuildArch: noarch
BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make
Requires: python3, (python3-jinja2 or python-jinja2), lsof
Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips
Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-impacket
%description
Portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
See release at https://github.com/9001/copyparty/releases
%global debug_package %{nil}
%generate_buildrequires
%pyproject_buildrequires
%prep
%setup -q
%build
cd "copyparty/web"
make
cd -
%pyproject_wheel
%install
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_libdir}/systemd/{system,user}
mkdir -p %{buildroot}/etc/%{name}
mkdir -p %{buildroot}/var/lib/%{name}-jail
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
%pyproject_install
%pyproject_save_files copyparty
install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh
install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf
install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service
install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service
install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service
install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md
install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
%files -n copyparty -f %{pyproject_files}
%license LICENSE
%{_bindir}/copyparty
%{_bindir}/partyfuse
%{_bindir}/u2c
%{_bindir}/prisonpary.sh
/etc/%{name}/%{name}.conf
%{_libdir}/systemd/system/%{name}@.service
%{_libdir}/systemd/user/%{name}.service
%{_libdir}/systemd/system/prisonparty@.service
/var/lib/%{name}-jail/README.md

View file

@ -0,0 +1,26 @@
# this will start `/usr/bin/copyparty`
# and read config from `$HOME/.config/copyparty.conf`
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
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'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=default.target

View file

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

View file

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

View file

@ -0,0 +1,30 @@
# this will start `/usr/bin/copyparty`
# and read config from `/etc/copyparty/copyparty.conf`
#
# the %i refers to whatever you put after the copyparty@
# so with copyparty@foo.service, %i == foo
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
WorkingDirectory=/var/lib/copyparty-jail
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
User=%i
Environment=XDG_CONFIG_HOME=/home/%i/.config
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=multi-user.target

10
contrib/systemd/index.md Normal file
View file

@ -0,0 +1,10 @@
this is `/var/lib/copyparty-jail`, 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
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
the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html
or by running `copyparty --help`

View file

@ -0,0 +1,38 @@
# this will start `/usr/bin/copyparty`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty/copyparty.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `%i` and `--`
#
# `%i %i` = user/group to run copyparty as; can be IDs (1000 1000)
# the %i refers to whatever you put after the prisonparty@
# so with prisonparty@foo.service, %i == foo
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=prisonparty
WorkingDirectory=/var/lib/copyparty-jail
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
User=%i
Environment=XDG_CONFIG_HOME=/home/%i/.config
# run copyparty
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \
/etc/copyparty \
-- \
/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=multi-user.target

View file

@ -63,10 +63,6 @@ web/browser.js
web/browser2.html web/browser2.html
web/cf.html web/cf.html
web/copyparty.gif web/copyparty.gif
web/dd/2.png
web/dd/3.png
web/dd/4.png
web/dd/5.png
web/deps/busy.mp3 web/deps/busy.mp3
web/deps/easymde.css web/deps/easymde.css
web/deps/easymde.js web/deps/easymde.js

View file

@ -53,13 +53,13 @@ from .util import (
PYFTPD_VER, PYFTPD_VER,
RAM_AVAIL, RAM_AVAIL,
RAM_TOTAL, RAM_TOTAL,
RE_ANSI,
SQLITE_VER, SQLITE_VER,
UNPLICATIONS, UNPLICATIONS,
URL_BUG, URL_BUG,
URL_PRJ, URL_PRJ,
Daemon, Daemon,
align_tab, align_tab,
ansi_re,
b64enc, b64enc,
dedent, dedent,
has_resource, has_resource,
@ -93,6 +93,10 @@ u = unicode
printed: list[str] = [] printed: list[str] = []
zsid = uuid.uuid4().urn[4:] zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
CFG_DEF.pop()
class RiceFormatter(argparse.HelpFormatter): class RiceFormatter(argparse.HelpFormatter):
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
@ -167,7 +171,7 @@ def lprint(*a: Any, **ka: Any) -> None:
txt: str = " ".join(unicode(x) for x in a) + eol txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt) printed.append(txt)
if not VT100: if not VT100:
txt = ansi_re.sub("", txt) txt = RE_ANSI.sub("", txt)
print(txt, end="", **ka) print(txt, end="", **ka)
@ -432,6 +436,40 @@ def args_from_cfg(cfg_path: str) -> list[str]:
return ret return ret
def expand_cfg(argv) -> list[str]:
if CFG_DEF:
supp = args_from_cfg(CFG_DEF[0])
argv = supp + argv
n = spins = 0
while n < len(argv):
if not n:
if spins % 1000 == 999:
t = "still expanding config files... giving up after %d more"
print(t % (9999 - spins))
if spins > 9999:
t = "got stuck expanding config files; do you have a config-file which imports itself? this is where I gave up:\n%r"
raise Exception(t % (argv[:1000]))
v1 = argv[n]
v1v = v1[2:].lstrip("=")
try:
v2 = argv[n + 1]
except:
v2 = ""
if v1 == "-c" and v2 and os.path.isfile(v2):
argv = argv[:n] + args_from_cfg(v2) + argv[n + 2 :]
spins += 1
n = 0
elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
argv = argv[:n] + args_from_cfg(v1v) + argv[n + 1 :]
spins += 1
n = 0
else:
n += 1
return argv
def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None: def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None:
msg = [""] * 5 msg = [""] * 5
for th in threading.enumerate(): for th in threading.enumerate():
@ -532,7 +570,7 @@ def get_sects():
dedent( dedent(
""" """
\033[33m-i\033[0m takes a comma-separated list of interfaces to listen on; \033[33m-i\033[0m takes a comma-separated list of interfaces to listen on;
IP-addresses and/or unix-sockets (Unix Domain Sockets) IP-addresses, unix-sockets, and/or open file descriptors
the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses
@ -547,17 +585,20 @@ def get_sects():
when running behind a reverse-proxy, it's recommended to when running behind a reverse-proxy, it's recommended to
use unix-sockets for improved performance and security; use unix-sockets for improved performance and security;
\033[32m-i unix:770:www:\033[33m/tmp/a.sock\033[0m listens on \033[33m/tmp/a.sock\033[0m with \033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on
permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m \033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m;
group. This is the best approach. Alternatively, only accessible to members of the \033[33mwww\033[0m group.
This is the best approach. Alternatively,
\033[32m-i unix:777:\033[33m/tmp/a.sock\033[0m sets perms \033[33m0777\033[0m so anyone can \033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone
access it; bad unless it's inside a restricted folder can access it; bad unless it's inside a restricted folder
\033[32m-i unix:\033[33m/tmp/a.sock\033[0m keeps umask-defined permissions \033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission
(usually \033[33m0600\033[0m) and the same user/group as copyparty (usually \033[33m0600\033[0m) and the same user/group as copyparty
\033[33m-p\033[0m (tcp ports) is ignored for unix sockets \033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3
\033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs
""" """
), ),
], ],
@ -573,7 +614,7 @@ def get_sects():
--grp takes groupname:username1,username2,... --grp takes groupname:username1,username2,...
and groupnames can be used instead of usernames in -v and groupnames can be used instead of usernames in -v
by prefixing the groupname with % by prefixing the groupname with @
list of permissions: list of permissions:
"r" (read): list folder contents, download files "r" (read): list folder contents, download files
@ -602,8 +643,41 @@ def get_sects():
if no accounts or volumes are configured, if no accounts or volumes are configured,
current folder will be read/write for everyone current folder will be read/write for everyone
the group @acct will always have every user with an account
(the name of that group can be changed with --grp-all)
consider the config file for more flexible account/volume management, consider the config file for more flexible account/volume management,
including dynamic reload at runtime (and being more readable w) including dynamic reload at runtime (and being more readable w)
see \033[32m--help-auth\033[0m for ways to provide the password in requests;
see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
"""
),
],
[
"auth",
"how to login from a client",
dedent(
"""
different ways to provide the password so you become authenticated:
login with the ui:
go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there
send the password in the '\033[36mPW\033[0m' http-header:
\033[36mPW: \033[35mhunter2\033[0m
or if you have \033[33m--accounts\033[0m enabled,
\033[36mPW: \033[35med:hunter2\033[0m
send the password in the URL itself:
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
or if you have \033[33m--accounts\033[0m enabled,
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
use basic-authentication:
\033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
which should be the same as this header:
\033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
""" """
), ),
], ],
@ -755,6 +829,36 @@ def get_sects():
the upload speed can easily drop to 10% for small files)""" the upload speed can easily drop to 10% for small files)"""
), ),
], ],
[
"idp",
"replacing the login system with fancy middleware",
dedent(
"""
if you already have a centralized service which handles
user-authentication for other services already, you can
integrate copyparty with that for automatic login
if the middleware is providing the username in an http-header
named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m
if the middleware is providing a list of groups in the header
named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m
if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m
if the middleware is providing a header named '\033[35mAccount\033[0m'
and the value is '\033[35malice@forest.net\033[0m' but the username is
actually '\033[35mmarisa\033[0m' then do this for each user:
\033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
(the separator '\033[35m^\033[0m' can be any character)
make ABSOLUTELY SURE that the header can only be set by your
middleware and not by clients! and, as an extra precaution,
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
"""
),
],
[ [
"urlform", "urlform",
"how to handle url-form POSTs", "how to handle url-form POSTs",
@ -872,31 +976,31 @@ def get_sects():
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-digit octal number such as 755, 750, 644, etc. the value is a three-digit octal number such as \033[32m755\033[0m, \033[32m750\033[0m, \033[32m644\033[0m, etc.
first digit = "User"; permission for the unix-user first digit = "User"; permission for the unix-user
second digit = "Group"; permission for the unix-group second digit = "Group"; permission for the unix-group
third digit = "Other"; permission for all other users/groups third digit = "Other"; permission for all other users/groups
for files: for files:
0 = --- = no access \033[32m0\033[0m = \033[35m---\033[0m = no access
1 = --x = can execute the file as a program \033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
2 = -w- = can write \033[32m2\033[0m = \033[35m-w-\033[0m = can write
3 = -wx = can write and execute \033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
4 = r-- = can read \033[32m4\033[0m = \033[35mr--\033[0m = can read
5 = r-x = can read and execute \033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
6 = rw- = can read and write \033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
7 = rwx = can read, write, execute \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute
for directories/folders: for directories/folders:
0 = --- = no access \033[32m0\033[0m = \033[35m---\033[0m = no access
1 = --x = can read files in folder but not list contents \033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
2 = -w- = n/a \033[32m2\033[0m = \033[35m-w-\033[0m = n/a
3 = -wx = can create files but not list \033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
4 = r-- = can list, but not read/write \033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
5 = r-x = can list and read files \033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
6 = rw- = n/a \033[32m6\033[0m = \033[35mrw-\033[0m = n/a
7 = rwx = can read, write, list \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
""" """
), ),
], ],
@ -911,6 +1015,9 @@ def get_sects():
copyparty will also hash and print any passwords that are non-hashed copyparty will also hash and print any passwords that are non-hashed
(password which do not start with '+') and then terminate afterwards (password which do not start with '+') and then terminate afterwards
if you have enabled --usernames then the password
must be provided as username:password for hashing
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a \033[36m--ah-alg\033[0m specifies the hashing algorithm and a
list of optional comma-separated arguments: list of optional comma-separated arguments:
@ -988,18 +1095,19 @@ def build_flags_desc():
def add_general(ap, nc, srvname): def add_general(ap, nc, srvname):
ap2 = ap.add_argument_group('general options') ap2 = ap.add_argument_group("general options")
ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file") ap2.add_argument("-c", metavar="PATH", type=u, default=CFG_DEF, action="append", help="\033[34mREPEATABLE:\033[0m add config file")
ap2.add_argument("-nc", metavar="NUM", type=int, default=nc, help="max num clients") ap2.add_argument("-nc", metavar="NUM", type=int, default=nc, help="max num clients")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]") ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]") ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)") ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
ap2.add_argument("--license", action="store_true", help="show licenses and exit") ap2.add_argument("--license", action="store_true", help="show licenses and exit")
@ -1007,15 +1115,16 @@ def add_general(ap, nc, srvname):
def add_qr(ap, tty): def add_qr(ap, tty):
ap2 = ap.add_argument_group('qr options') ap2 = ap.add_argument_group("qr options")
ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup") ap2.add_argument("--qr", action="store_true", help="show QR-code on startup")
ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup") ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://")
ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]") ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead") ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable") ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable")
ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)") ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)") ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
def add_fs(ap): def add_fs(ap):
@ -1029,7 +1138,7 @@ def add_fs(ap):
def add_share(ap): def add_share(ap):
db_path = os.path.join(E.cfg, "shares.db") db_path = os.path.join(E.cfg, "shares.db")
ap2 = ap.add_argument_group('share-url options') ap2 = ap.add_argument_group("share-url options")
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]") ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in") ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share") ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
@ -1038,13 +1147,14 @@ def add_share(ap):
def add_upload(ap): def add_upload(ap):
ap2 = ap.add_argument_group('upload options') ap2 = ap.add_argument_group("upload options")
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m") ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip") ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)") ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)")
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)") ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)") ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)")
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") ap2.add_argument("--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")
@ -1052,10 +1162,13 @@ def add_upload(ap):
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("--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-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[32m777\033[0m] = full-yolo (volflag=chmod_d)") ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--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("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)") ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)") ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)") ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
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("--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("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)") 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-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)") 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)")
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually") ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
@ -1077,14 +1190,14 @@ def add_upload(ap):
def add_network(ap): def add_network(ap):
ap2 = ap.add_argument_group('network options') ap2 = ap.add_argument_group("network options")
ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6") ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (comma-separated list; see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets")
ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m2\033[0m]=outermost-proxy, [\033[32m3\033[0m]=second-proxy, [\033[32m-1\033[0m]=closest-proxy") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=9999999, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m-1\033[0m]=closest-proxy, [\033[32m-2\033[0m]=second-hop, [\033[32m-3\033[0m]=third-hop")
ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from")
ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="comma-separated list of trusted reverse-proxy CIDRs; only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
if ANYWIN: if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances") ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
@ -1102,10 +1215,10 @@ def add_network(ap):
def add_tls(ap, cert_path): def add_tls(ap, cert_path):
ap2 = ap.add_argument_group('SSL/TLS options') ap2 = ap.add_argument_group("SSL/TLS options")
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext") ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls") ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to TLS certificate") ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain")
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe") ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers") ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
@ -1114,7 +1227,7 @@ def add_tls(ap, cert_path):
def add_cert(ap, cert_path): def add_cert(ap, cert_path):
cert_dir = os.path.dirname(cert_path) cert_dir = os.path.dirname(cert_path)
ap2 = ap.add_argument_group('TLS certificate generator options') ap2 = ap.add_argument_group("TLS certificate generator options")
ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation") ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation")
ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate") ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate")
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m") ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m")
@ -1134,27 +1247,33 @@ def add_cert(ap, cert_path):
def add_auth(ap): def add_auth(ap):
idp_db = os.path.join(E.cfg, "idp.db") idp_db = os.path.join(E.cfg, "idp.db")
ses_db = os.path.join(E.cfg, "sessions.db") ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options') ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
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=20, help="session key length; default is 120 bits ((20//4)*4*6)")
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies") ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="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]") 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]")
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
def add_chpw(ap): def add_chpw(ap):
db_path = os.path.join(E.cfg, "chpw.json") db_path = os.path.join(E.cfg, "chpw.json")
ap2 = ap.add_argument_group('user-changeable passwords options') ap2 = ap.add_argument_group("user-changeable passwords options")
ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords") ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords")
ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="do not allow password-changes for this comma-separated list of usernames") ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m do not allow password-changes for this comma-separated list of usernames")
ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length") ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length")
ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users") ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users")
@ -1186,6 +1305,7 @@ def add_zc_mdns(ap):
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares") ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares") ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares") ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network") ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not") ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty") ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
@ -1203,12 +1323,12 @@ def add_zc_ssdp(ap):
def add_ftp(ap): def add_ftp(ap):
ap2 = ap.add_argument_group('FTP options (TCP only)') ap2 = ap.add_argument_group("FTP options (TCP only)")
ap2.add_argument("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921") ap2.add_argument("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921")
ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990") ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990")
ap2.add_argument("--ftpv", action="store_true", help="verbose") ap2.add_argument("--ftpv", action="store_true", help="verbose")
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ftp-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. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite") ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite")
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-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-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections")
@ -1216,7 +1336,7 @@ def add_ftp(ap):
def add_webdav(ap): def add_webdav(ap):
ap2 = ap.add_argument_group('WebDAV options') ap2 = ap.add_argument_group("WebDAV options")
ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)") ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)")
ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this") ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this")
ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)") ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)")
@ -1226,7 +1346,7 @@ def add_webdav(ap):
def add_tftp(ap): def add_tftp(ap):
ap2 = ap.add_argument_group('TFTP options (UDP only)') ap2 = ap.add_argument_group("TFTP options (UDP only)")
ap2.add_argument("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969") ap2.add_argument("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969")
ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--tftpv", action="store_true", help="verbose") ap2.add_argument("--tftpv", action="store_true", help="verbose")
@ -1234,12 +1354,12 @@ def add_tftp(ap):
ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations") ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations")
ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...") ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...")
ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing") ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing")
ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--tftp-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. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000") ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000")
def add_smb(ap): def add_smb(ap):
ap2 = ap.add_argument_group('SMB/CIFS options') ap2 = ap.add_argument_group("SMB/CIFS options")
ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!") ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!")
ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)") ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)")
ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)") ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)")
@ -1253,30 +1373,30 @@ def add_smb(ap):
def add_handlers(ap): def add_handlers(ap):
ap2 = ap.add_argument_group('handlers (see --help-handlers)') ap2 = ap.add_argument_group("handlers (see --help-handlers)")
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="handle 404s by executing \033[33mPY\033[0m file") ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file")
ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing \033[33mPY\033[0m file") ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 403s by executing \033[33mPY\033[0m file")
ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff") ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff")
def add_hooks(ap): def add_hooks(ap):
ap2 = ap.add_argument_group('event hooks (see --help-hooks)') ap2 = ap.add_argument_group("event hooks (see --help-hooks)")
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file upload starts") ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file upload starts")
ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file upload finishes") ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file upload finishes")
ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after all uploads finish and volume is idle") ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after all uploads finish and volume is idle")
ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file copy") ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file copy")
ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file copy") ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file copy")
ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file move/rename") ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file move/rename")
ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file move/rename") ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file move/rename")
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m before a file delete") ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m after a file delete") ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m on message") ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m on message")
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)") ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)")
ap2.add_argument("--hook-v", action="store_true", help="verbose hooks") ap2.add_argument("--hook-v", action="store_true", help="verbose hooks")
def add_stats(ap): def add_stats(ap):
ap2 = ap.add_argument_group('grafana/prometheus metrics endpoint') ap2 = ap.add_argument_group("grafana/prometheus metrics endpoint")
ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts") ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts")
ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)") ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)")
ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)") ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)")
@ -1286,20 +1406,23 @@ def add_stats(ap):
def add_yolo(ap): def add_yolo(ap):
ap2 = ap.add_argument_group('yolo options') ap2 = ap.add_argument_group("yolo options")
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests") ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
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("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
def add_optouts(ap): def add_optouts(ap):
ap2 = ap.add_argument_group('opt-outs') ap2 = ap.add_argument_group("opt-outs")
ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)") ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)") ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)")
ap2.add_argument("--no-dav", action="store_true", help="disable webdav support") ap2.add_argument("--no-dav", action="store_true", help="disable webdav support")
ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("--no-cp", action="store_true", help="disable copy operations") ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>") ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI") ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
@ -1319,9 +1442,9 @@ def add_optouts(ap):
def add_safety(ap): def add_safety(ap):
ap2 = ap.add_argument_group('safety options') ap2 = ap.add_argument_group("safety options")
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js") ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih") ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih")
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r") ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]") ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)") ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
@ -1335,6 +1458,7 @@ def add_safety(ap):
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month") ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)") ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
@ -1342,6 +1466,8 @@ def add_safety(ap):
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy") ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
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("--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("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives") ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
@ -1349,7 +1475,7 @@ def add_safety(ap):
def add_salt(ap, fk_salt, dk_salt, ah_salt): def add_salt(ap, fk_salt, dk_salt, ah_salt):
ap2 = ap.add_argument_group('salting options') 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-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)") ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)")
ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]") ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
@ -1363,23 +1489,23 @@ def add_salt(ap, fk_salt, dk_salt, ah_salt):
def add_shutdown(ap): def add_shutdown(ap):
ap2 = ap.add_argument_group('shutdown options') ap2 = ap.add_argument_group("shutdown options")
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints") ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all") ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all")
ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing") ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing")
def add_logging(ap): def add_logging(ap):
ap2 = ap.add_argument_group('logging options') ap2 = ap.add_argument_group("logging options")
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages") ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("-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("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)") ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=1, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
@ -1388,7 +1514,7 @@ def add_logging(ap):
def add_admin(ap): def add_admin(ap):
ap2 = ap.add_argument_group('admin panel options') ap2 = ap.add_argument_group("admin panel options")
ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)") ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)") ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)") ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
@ -1402,17 +1528,18 @@ def add_admin(ap):
def add_thumbnail(ap): def add_thumbnail(ap):
th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6 th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6
th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10 th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10
ap2 = ap.add_argument_group('thumbnail options') ap2 = ap.add_argument_group("thumbnail options")
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)") ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)")
ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
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-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-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-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-webp", action="store_true", help="disable webp 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-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
@ -1421,22 +1548,27 @@ def add_thumbnail(ap):
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") 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-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips # https://github.com/libvips/libvips
# https://stackoverflow.com/a/47612661
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,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,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="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,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-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)") ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg") ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
def add_transcoding(ap): def add_transcoding(ap):
ap2 = ap.add_argument_group('transcoding options') ap2 = ap.add_argument_group("transcoding options")
ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable") ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable")
ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable") ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable")
ap2.add_argument("--allow-wav", action="store_true", help="allow transcoding to wav (lossless, uncompressed)")
ap2.add_argument("--allow-flac", action="store_true", help="allow transcoding to flac (lossless, compressed)")
ap2.add_argument("--no-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead") ap2.add_argument("--no-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead")
ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead") ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead")
ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding") ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
@ -1445,7 +1577,7 @@ def add_transcoding(ap):
def add_tail(ap): def add_tail(ap):
ap2 = ap.add_argument_group('tailing options (realtime streaming of a growing file)') ap2 = ap.add_argument_group("tailing options (realtime streaming of a growing file)")
ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)") ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)")
ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads") ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)") ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
@ -1455,7 +1587,7 @@ def add_tail(ap):
def add_rss(ap): def add_rss(ap):
ap2 = ap.add_argument_group('RSS options') ap2 = ap.add_argument_group("RSS options")
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)") ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)")
ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')") ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')")
ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all") ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all")
@ -1464,7 +1596,7 @@ def add_rss(ap):
def add_db_general(ap, hcores): def add_db_general(ap, hcores):
noidx = APPLESAN_TXT if MACOS else "" noidx = APPLESAN_TXT if MACOS else ""
ap2 = ap.add_argument_group('general db options') ap2 = ap.add_argument_group("general db options")
ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication") ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m") ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m") ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m")
@ -1473,8 +1605,8 @@ def add_db_general(ap, hcores):
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty") ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)") ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)") ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)")
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)") ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)")
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)") ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)") ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch") ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower") ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
@ -1493,7 +1625,7 @@ def add_db_general(ap, hcores):
def add_db_metadata(ap): def add_db_metadata(ap):
ap2 = ap.add_argument_group('metadata db options') ap2 = ap.add_argument_group("metadata db options")
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...") ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...")
ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m") ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m")
ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m") ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m")
@ -1503,15 +1635,16 @@ def add_db_metadata(ap):
ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning") ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning")
ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such") ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such")
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers") ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE) ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE)
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH) ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH)
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file") ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file")
def add_txt(ap): def add_txt(ap):
ap2 = ap.add_argument_group('textfile options') ap2 = ap.add_argument_group("textfile options")
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)") ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds") ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk") ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)") ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)")
@ -1521,7 +1654,7 @@ def add_txt(ap):
def add_og(ap): def add_og(ap):
ap2 = ap.add_argument_group('og / open graph / discord-embed options') ap2 = ap.add_argument_group("og / open graph / discord-embed options")
ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)") ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)")
ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)") ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)")
ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)") ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)")
@ -1539,22 +1672,24 @@ def add_og(ap):
def add_ui(ap, retry): def add_ui(ap, retry):
ap2 = ap.add_argument_group('ui options') THEMES = 10
ap2 = ap.add_argument_group("ui options")
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m") ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)") ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)") ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)") ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions") ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)") ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)") ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable") ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)") ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])") ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]") ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]")
ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html") 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-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
@ -1565,10 +1700,11 @@ def add_ui(ap, retry):
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)") ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents") ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-np\033[0m") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)") ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox") ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox")
ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)") ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)")
ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)") ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)")
@ -1579,7 +1715,7 @@ def add_ui(ap, retry):
def add_debug(ap): def add_debug(ap):
ap2 = ap.add_argument_group('debug options') ap2 = ap.add_argument_group("debug options")
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)") 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("--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") ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies")
@ -1744,16 +1880,7 @@ def main(argv: Optional[list[str]] = None) -> None:
ensure_webdeps() ensure_webdeps()
for k, v in zip(argv[1:], argv[2:]): argv = expand_cfg(argv)
if k == "-c" and os.path.isfile(v):
supp = args_from_cfg(v)
argv.extend(supp)
for k in argv[1:]:
v = k[2:]
if k.startswith("-c") and v and os.path.isfile(v):
supp = args_from_cfg(v)
argv.extend(supp)
deprecated: list[tuple[str, str]] = [ deprecated: list[tuple[str, str]] = [
("--salt", "--warksalt"), ("--salt", "--warksalt"),
@ -1779,7 +1906,7 @@ def main(argv: Optional[list[str]] = None) -> None:
argv[idx] = nk + ov argv[idx] = nk + ov
time.sleep(2) time.sleep(2)
da = len(argv) == 1 da = len(argv) == 1 and not CFG_DEF
try: try:
if da: if da:
argv.extend(["--qr"]) argv.extend(["--qr"])

View file

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

View file

@ -33,6 +33,7 @@ from .util import (
afsenc, afsenc,
get_df, get_df,
humansize, humansize,
json_hesc,
min_ex, min_ex,
odfusion, odfusion,
read_utf8, read_utf8,
@ -70,6 +71,25 @@ if PY2:
LEELOO_DALLAS = "leeloo_dallas" LEELOO_DALLAS = "leeloo_dallas"
##
## you might be curious what Leeloo Dallas is doing here, so let me explain:
##
## certain daemonic tasks, namely:
## * deletion of expired files, running on a timer
## * deletion of sidecar files, initiated by plugins
## need to skip the usual permission-checks to do their thing,
## so we let Leeloo handle these
##
## and also, the smb-server has really shitty support for user-accounts
## so one popular way to avoid issues is by running copyparty without users;
## this makes all smb-clients identify as LD to gain unrestricted access
##
## Leeloo, being a fictional character from The Fifth Element,
## obviously does not exist and will never be able to access any copyparty
## instances from the outside (the username is rejected at every entrypoint)
##
## thanks for coming to my ted talk
SEE_LOG = "see log for details" SEE_LOG = "see log for details"
SEESLOG = " (see serverlog for details)" SEESLOG = " (see serverlog for details)"
@ -121,6 +141,8 @@ class Lim(object):
self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry
self.chmod_d = 0o755 self.chmod_d = 0o755
self.uid = self.gid = -1
self.chown = False
self.nups: dict[str, list[float]] = {} # num tracker self.nups: dict[str, list[float]] = {} # num tracker
self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list
@ -283,6 +305,8 @@ class Lim(object):
# no branches yet; make one # no branches yet; make one
sub = os.path.join(path, "0") sub = os.path.join(path, "0")
bos.mkdir(sub, self.chmod_d) bos.mkdir(sub, self.chmod_d)
if self.chown:
os.chown(sub, self.uid, self.gid)
else: else:
# try newest branch only # try newest branch only
sub = os.path.join(path, str(dirs[-1])) sub = os.path.join(path, str(dirs[-1]))
@ -298,6 +322,8 @@ class Lim(object):
# make a branch # make a branch
sub = os.path.join(path, str(dirs[-1] + 1)) sub = os.path.join(path, str(dirs[-1] + 1))
bos.mkdir(sub, self.chmod_d) bos.mkdir(sub, self.chmod_d)
if self.chown:
os.chown(sub, self.uid, self.gid)
ret = self.dive(sub, lvs - 1) ret = self.dive(sub, lvs - 1)
if ret is None: if ret is None:
raise Pebkac(500, "rotation bug") raise Pebkac(500, "rotation bug")
@ -855,6 +881,15 @@ class VFS(object):
return None return None
if "xvol" in self.flags: if "xvol" in self.flags:
self_ap = self.realpath + os.sep
if aps.startswith(self_ap):
vp = aps[len(self_ap) :]
if ANYWIN:
vp = vp.replace(os.sep, "/")
vn2, _ = self._find(vp)
if self == vn2:
return self
all_aps = self.shr_all_aps or self.root.all_aps all_aps = self.shr_all_aps or self.root.all_aps
for vap, vns in all_aps: for vap, vns in all_aps:
@ -1073,6 +1108,9 @@ class AuthSrv(object):
if rejected: if rejected:
continue continue
if gn == self.args.grp_all:
gn = ""
# if ap/vp has a user/group placeholder, make sure to keep # if ap/vp has a user/group placeholder, make sure to keep
# track so the same user/group is mapped when setting perms; # track so the same user/group is mapped when setting perms;
# otherwise clear un/gn to indicate it's a regular volume # otherwise clear un/gn to indicate it's a regular volume
@ -1182,6 +1220,7 @@ class AuthSrv(object):
self.load_idp_db(bool(self.idp_accs)) self.load_idp_db(bool(self.idp_accs))
ret = {un: gns[:] for un, gns in self.idp_accs.items()} ret = {un: gns[:] for un, gns in self.idp_accs.items()}
ret.update({zs: [""] for zs in acct if zs not in ret}) ret.update({zs: [""] for zs in acct if zs not in ret})
grps[self.args.grp_all] = list(ret.keys())
for gn, uns in grps.items(): for gn, uns in grps.items():
for un in uns: for un in uns:
try: try:
@ -1659,6 +1698,9 @@ class AuthSrv(object):
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns))) self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
raise raise
self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
self.setup_pwhash(acct) self.setup_pwhash(acct)
defpw = acct.copy() defpw = acct.copy()
self.setup_chpw(acct) self.setup_chpw(acct)
@ -1671,9 +1713,10 @@ class AuthSrv(object):
mount = cased mount = cased
if not mount and not self.args.idp_h_usr: if not mount and not self.args.have_idp_hdrs:
# -h says our defaults are CWD at root and read/write for everyone # -h says our defaults are CWD at root and read/write for everyone
axs = AXS(["*"], ["*"], None, None) axs = AXS(["*"], ["*"], None, None)
ehint = ""
if self.is_lxc: if self.is_lxc:
t = "Read-access has been disabled due to failsafe: Docker detected, but %s. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to all of /w/ by adding the following arguments to the docker container: -v .::rw" t = "Read-access has been disabled due to failsafe: Docker detected, but %s. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to all of /w/ by adding the following arguments to the docker container: -v .::rw"
if len(cfg_files_loaded) == 1: if len(cfg_files_loaded) == 1:
@ -1683,11 +1726,23 @@ class AuthSrv(object):
else: else:
self.log(t % ("the config does not define any volumes",), 1) self.log(t % ("the config does not define any volumes",), 1)
axs = AXS() axs = AXS()
ehint = "; please try moving them up one level, into the parent folder:"
elif self.args.c: elif self.args.c:
t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw" t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
self.log(t, 1) self.log(t, 1)
axs = AXS() axs = AXS()
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0()) ehint = ":"
if ehint:
try:
files = os.listdir(E.cfg)
except:
files = []
hits = [x for x in files if x.lower().endswith(".conf")]
if hits:
t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n"
self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
zvf = {"tcolor": self.args.tcolor}
vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
if not axs.uread: if not axs.uread:
self.badcfg1 = True self.badcfg1 = True
elif "" not in mount: elif "" not in mount:
@ -1831,7 +1886,7 @@ class AuthSrv(object):
if missing_users: if missing_users:
zs = ", ".join(k for k in sorted(missing_users)) zs = ", ".join(k for k in sorted(missing_users))
if self.args.idp_h_usr: if self.args.have_idp_hdrs:
t = "the following users are unknown, and assumed to come from IdP: " t = "the following users are unknown, and assumed to come from IdP: "
self.log(t + zs, c=6) self.log(t + zs, c=6)
else: else:
@ -1842,6 +1897,16 @@ class AuthSrv(object):
if LEELOO_DALLAS in all_users: if LEELOO_DALLAS in all_users:
raise Exception("sorry, reserved username: " + LEELOO_DALLAS) raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
zsl = []
for usr in list(acct)[:]:
zs = acct[usr].strip()
if not zs:
zs = ub64enc(os.urandom(48)).decode("ascii")
zsl.append(usr)
acct[usr] = zs
if zsl:
self.log("generated random passwords for users %r" % (zsl,), 6)
seenpwds = {} seenpwds = {}
for usr, pwd in acct.items(): for usr, pwd in acct.items():
if pwd in seenpwds: if pwd in seenpwds:
@ -2124,6 +2189,7 @@ class AuthSrv(object):
all_mte = {} all_mte = {}
errors = False errors = False
free_umask = False free_umask = False
have_reflink = False
for vol in vfs.all_nodes.values(): for vol in vfs.all_nodes.values():
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa: if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True vol.flags["e2ds"] = True
@ -2161,12 +2227,12 @@ class AuthSrv(object):
if vf not in vol.flags: if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga) vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip nrand tail_who u2abort u2ow ups_who zip_who" zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
for k in zs.split(): for k in zs.split():
if k in vol.flags: if k in vol.flags:
vol.flags[k] = int(vol.flags[k]) vol.flags[k] = int(vol.flags[k])
zs = "convt tail_fd tail_rate tail_tmax" zs = "aconvt convt tail_fd tail_rate tail_tmax"
for k in zs.split(): for k in zs.split():
if k in vol.flags: if k in vol.flags:
vol.flags[k] = float(vol.flags[k]) vol.flags[k] = float(vol.flags[k])
@ -2198,8 +2264,17 @@ class AuthSrv(object):
if (is_d and zi != 0o755) or not is_d: if (is_d and zi != 0o755) or not is_d:
free_umask = True free_umask = True
vol.flags.pop("chown", None)
if vol.flags["uid"] != -1 or vol.flags["gid"] != -1:
vol.flags["chown"] = True
vol.flags.pop("fperms", None)
if "chown" in vol.flags or vol.flags.get("chmod_f"):
vol.flags["fperms"] = True
if vol.lim: if vol.lim:
vol.lim.chmod_d = vol.flags["chmod_d"] vol.lim.chmod_d = vol.flags["chmod_d"]
vol.lim.chown = "chown" in vol.flags
vol.lim.uid = vol.flags["uid"]
vol.lim.gid = vol.flags["gid"]
if vol.flags.get("og"): if vol.flags.get("og"):
self.args.uqe = True self.args.uqe = True
@ -2207,6 +2282,9 @@ class AuthSrv(object):
if "unlistcr" in vol.flags or "unlistcw" in vol.flags: if "unlistcr" in vol.flags or "unlistcw" in vol.flags:
self.args.have_unlistc = True self.args.have_unlistc = True
if "reflink" in vol.flags:
have_reflink = True
zs = str(vol.flags.get("tcolor", "")).lstrip("#") zs = str(vol.flags.get("tcolor", "")).lstrip("#")
if len(zs) == 3: # fc5 => ffcc55 if len(zs) == 3: # fc5 => ffcc55
vol.flags["tcolor"] = "".join([x * 2 for x in zs]) vol.flags["tcolor"] = "".join([x * 2 for x in zs])
@ -2485,7 +2563,7 @@ class AuthSrv(object):
if not self.args.no_voldump: if not self.args.no_voldump:
self.log(t) self.log(t)
if have_e2d or self.args.idp_h_usr: if have_e2d or self.args.have_idp_hdrs:
t = self.chk_sqlite_threadsafe() t = self.chk_sqlite_threadsafe()
if t: if t:
self.log("\n\033[{}\033[0m\n".format(t)) self.log("\n\033[{}\033[0m\n".format(t))
@ -2571,6 +2649,13 @@ class AuthSrv(object):
t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume." t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
self.log(t + "".join(self.idp_err), 1) self.log(t + "".join(self.idp_err), 1)
if have_reflink:
t = "WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead."
if sys.version_info < (3, 14):
self.log(t % "your python version is not new enough", 1)
if not sys.platform.startswith("linux"):
self.log(t % "your OS is not Linux", 1)
self.vfs = vfs self.vfs = vfs
self.acct = acct self.acct = acct
self.defpw = defpw self.defpw = defpw
@ -2583,6 +2668,8 @@ class AuthSrv(object):
self.re_pwd = None self.re_pwd = None
pwds = [re.escape(x) for x in self.iacct.keys()] pwds = [re.escape(x) for x in self.iacct.keys()]
pwds.extend(list(self.sesa)) pwds.extend(list(self.sesa))
if self.args.usernames:
pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x])
if pwds: if pwds:
if self.ah.on: if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)" zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@ -2705,6 +2792,8 @@ class AuthSrv(object):
"s_name": self.args.bname, "s_name": self.args.bname,
"have_up2k_idx": "e2d" in vf, "have_up2k_idx": "e2d" in vf,
"have_acode": not self.args.no_acode, "have_acode": not self.args.no_acode,
"have_c2flac": self.args.allow_flac,
"have_c2wav": self.args.allow_wav,
"have_shr": self.args.shr, "have_shr": self.args.shr,
"have_zip": not self.args.no_zip, "have_zip": not self.args.no_zip,
"have_mv": not self.args.no_mv, "have_mv": not self.args.no_mv,
@ -2729,6 +2818,7 @@ class AuthSrv(object):
"dth3x": vf["th3x"], "dth3x": vf["th3x"],
"dvol": self.args.au_vol, "dvol": self.args.au_vol,
"idxh": int(self.args.ih), "idxh": int(self.args.ih),
"dutc": not self.args.localtime,
"themes": self.args.themes, "themes": self.args.themes,
"turbolvl": self.args.turbo, "turbolvl": self.args.turbo,
"nosubtle": self.args.nosubtle, "nosubtle": self.args.nosubtle,
@ -2740,7 +2830,7 @@ class AuthSrv(object):
"lifetime": vn.js_ls["lifetime"], "lifetime": vn.js_ls["lifetime"],
"u2sort": self.args.u2sort, "u2sort": self.args.u2sort,
} }
vn.js_htm = json.dumps(js_htm) vn.js_htm = json_hesc(json.dumps(js_htm))
vols = list(vfs.all_nodes.values()) vols = list(vfs.all_nodes.values())
if enshare: if enshare:
@ -2763,7 +2853,7 @@ class AuthSrv(object):
def load_idp_db(self, quiet=False) -> None: def load_idp_db(self, quiet=False) -> None:
# mutex me # mutex me
level = self.args.idp_store level = self.args.idp_store
if level < 2 or not self.args.idp_h_usr: if level < 2 or not self.args.have_idp_hdrs:
return return
assert sqlite3 # type: ignore # !rm assert sqlite3 # type: ignore # !rm
@ -2819,7 +2909,10 @@ class AuthSrv(object):
n = [] n = []
q = "insert into us values (?,?,?)" q = "insert into us values (?,?,?)"
for uname in self.acct: accs = list(self.acct)
if self.args.have_idp_hdrs and self.args.idp_cookie:
accs.extend(self.idp_accs.keys())
for uname in accs:
if uname not in ases: if uname not in ases:
sid = ub64enc(os.urandom(blen)).decode("ascii") sid = ub64enc(os.urandom(blen)).decode("ascii")
cur.execute(q, (uname, sid, int(time.time()))) cur.execute(q, (uname, sid, int(time.time())))
@ -2877,6 +2970,9 @@ class AuthSrv(object):
t = "minimum password length: %d characters" t = "minimum password length: %d characters"
return False, t % (self.args.chpw_len,) return False, t % (self.args.chpw_len,)
if self.args.usernames:
pw = "%s:%s" % (uname, pw)
hpw = self.ah.hash(pw) if self.ah.on else pw hpw = self.ah.hash(pw) if self.ah.on else pw
if hpw == self.acct[uname]: if hpw == self.acct[uname]:
@ -2968,6 +3064,12 @@ class AuthSrv(object):
self.log("chpw: " + msg, 6) self.log("chpw: " + msg, 6)
def setup_pwhash(self, acct: dict[str, str]) -> None: def setup_pwhash(self, acct: dict[str, str]) -> None:
if self.args.usernames:
for uname, pw in list(acct.items())[:]:
if pw.startswith("+") and len(pw) == 33:
continue
acct[uname] = "%s:%s" % (uname, pw)
self.ah = PWHash(self.args) self.ah = PWHash(self.args)
if not self.ah.on: if not self.ah.on:
if self.args.ah_cli or self.args.ah_gen: if self.args.ah_cli or self.args.ah_gen:
@ -3411,7 +3513,7 @@ def expand_config_file(
ipath += " -> " + fp ipath += " -> " + fp
ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath)) ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
cfg_lines = read_utf8(log, fp, True).split("\n") cfg_lines = read_utf8(log, fp, True).replace("\t", " ").split("\n")
if True: # diff-golf if True: # diff-golf
for oln in [x.rstrip() for x in cfg_lines]: for oln in [x.rstrip() for x in cfg_lines]:
ln = oln.split(" #")[0].strip() ln = oln.split(" #")[0].strip()

View file

@ -9,8 +9,11 @@ from . import path as path
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional from typing import Any, Optional
_ = (path,) MKD_755 = {"chmod_d": 0o755}
__all__ = ["path"] MKD_700 = {"chmod_d": 0o700}
_ = (path, MKD_755, MKD_700)
__all__ = ["path", "MKD_755", "MKD_700"]
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@ -20,11 +23,15 @@ def chmod(p: str, mode: int) -> None:
return os.chmod(fsenc(p), mode) return os.chmod(fsenc(p), mode)
def chown(p: str, uid: int, gid: int) -> None:
return os.chown(fsenc(p), uid, gid)
def listdir(p: str = ".") -> list[str]: def listdir(p: str = ".") -> list[str]:
return [fsdec(x) for x in os.listdir(fsenc(p))] return [fsdec(x) for x in os.listdir(fsenc(p))]
def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool: def makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool:
# os.makedirs does 777 for all but leaf; this does mode on all # os.makedirs does 777 for all but leaf; this does mode on all
todo = [] todo = []
bname = fsenc(name) bname = fsenc(name)
@ -37,9 +44,13 @@ def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
if not exist_ok: if not exist_ok:
os.mkdir(bname) # to throw os.mkdir(bname) # to throw
return False return False
mode = vf["chmod_d"]
chown = "chown" in vf
for zb in todo[::-1]: for zb in todo[::-1]:
try: try:
os.mkdir(zb, mode) os.mkdir(zb, mode)
if chown:
os.chown(zb, vf["uid"], vf["gid"])
except: except:
if os.path.isdir(zb): if os.path.isdir(zb):
continue continue

View file

@ -52,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
"og_no_head", "og_no_head",
"og_s_title", "og_s_title",
"rand", "rand",
"reflink",
"rmagic", "rmagic",
"rss", "rss",
"wo_up_readme", "wo_up_readme",
@ -67,6 +68,7 @@ def vf_bmap() -> dict[str, str]:
def vf_vmap() -> dict[str, str]: def vf_vmap() -> dict[str, str]:
"""argv-to-volflag: simple values""" """argv-to-volflag: simple values"""
ret = { ret = {
"ac_convt": "aconvt",
"no_hash": "nohash", "no_hash": "nohash",
"no_idx": "noidx", "no_idx": "noidx",
"re_maxage": "scan", "re_maxage": "scan",
@ -110,9 +112,14 @@ def vf_vmap() -> dict[str, str]:
"tail_tmax", "tail_tmax",
"tail_who", "tail_who",
"tcolor", "tcolor",
"th_spec_p",
"txt_eol",
"unlist", "unlist",
"u2abort", "u2abort",
"u2ts", "u2ts",
"uid",
"gid",
"unp_who",
"ups_who", "ups_who",
"zip_who", "zip_who",
"zipmaxn", "zipmaxn",
@ -168,11 +175,14 @@ flagcats = {
"dedup": "enable symlink-based file deduplication", "dedup": "enable symlink-based file deduplication",
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible", "hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible", "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", "safededup": "verify on-disk data before using it for dedup",
"noclone": "take dupe data from clients, even if available on HDD", "noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)", "nodupe": "rejects existing files (instead of linking/cloning them)",
"chmod_d=755": "unix-permission for new dirs/folders", "chmod_d=755": "unix-permission for new dirs/folders",
"chmod_f=644": "unix-permission for new files", "chmod_f=644": "unix-permission for new files",
"uid=573": "change owner of new files/folders to unix-user 573",
"gid=999": "change owner of new files/folders to unix-group 999",
"sparse": "force use of sparse files, mainly for s3-backed storage", "sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage", "nosparse": "deny use of sparse files, mainly for slow storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
@ -253,7 +263,9 @@ flagcats = {
"thsize": "thumbnail res; WxH", "thsize": "thumbnail res; WxH",
"crop": "center-cropping (y/n/fy/fn)", "crop": "center-cropping (y/n/fy/fn)",
"th3x": "3x resolution (y/n/fy/fn)", "th3x": "3x resolution (y/n/fy/fn)",
"convt": "conversion timeout in seconds", "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",
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s", "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
}, },
"handlers\n(better explained in --help-handlers)": { "handlers\n(better explained in --help-handlers)": {
@ -316,6 +328,7 @@ flagcats = {
"exp": "enable textfile expansion; see --help-exp", "exp": "enable textfile expansion; see --help-exp",
"exp_md": "placeholders to expand in markdown files; see --help", "exp_md": "placeholders to expand in markdown files; see --help",
"exp_lg": "placeholders to expand in prologue/epilogue; see --help", "exp_lg": "placeholders to expand in prologue/epilogue; see --help",
"txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
}, },
"tailing": { "tailing": {
"notail": "disable ?tail (download a growing file continuously)", "notail": "disable ?tail (download a growing file continuously)",
@ -333,6 +346,7 @@ flagcats = {
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
"rss": "allow '?rss' URL suffix (experimental)", "rss": "allow '?rss' URL suffix (experimental)",
"rmagic": "expensive analysis for mimetype accuracy", "rmagic": "expensive analysis for mimetype accuracy",
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
"ups_who=2": "restrict viewing the list of recent uploads", "ups_who=2": "restrict viewing the list of recent uploads",
"zip_who=2": "restrict access to download-as-zip/tar", "zip_who=2": "restrict access to download-as-zip/tar",
"zipmaxn=9k": "reject download-as-zip if more than 9000 files", "zipmaxn=9k": "reject download-as-zip if more than 9000 files",

View file

@ -65,6 +65,9 @@ DXMLParser = _DXMLParser
def parse_xml(txt: str) -> ET.Element: def parse_xml(txt: str) -> ET.Element:
"""
Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
"""
parser = DXMLParser() parser = DXMLParser()
parser.feed(txt) parser.feed(txt)
return parser.close() # type: ignore return parser.close() # type: ignore

View file

@ -31,6 +31,7 @@ from .util import (
relchk, relchk,
runhook, runhook,
sanitize_fn, sanitize_fn,
set_fperms,
vjoin, vjoin,
wunlink, wunlink,
) )
@ -82,7 +83,12 @@ class FtpAuth(DummyAuthorizer):
uname = "*" uname = "*"
if username != "anonymous": if username != "anonymous":
uname = "" uname = ""
for zs in (password, username): if args.usernames:
alts = ["%s:%s" % (username, password)]
else:
alts = password, username
for zs in alts:
zs = asrv.iacct.get(asrv.ah.hash(zs), "") zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs: if zs:
uname = zs uname = zs
@ -90,6 +96,10 @@ class FtpAuth(DummyAuthorizer):
if args.ipu and uname == "*": if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(ip)] uname = args.ipu_iu[args.ipu_nm.map(ip)]
if args.ipr and uname in args.ipr_u:
if not args.ipr_u[uname].map(ip):
logging.warning("username [%s] rejected by --ipr", uname)
uname = "*"
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd g = self.hub.gpwd
@ -262,8 +272,8 @@ class FtpFs(AbstractedFS):
wunlink(self.log, ap, VF_CAREFUL) wunlink(self.log, ap, VF_CAREFUL)
ret = open(fsenc(ap), mode, self.args.iobuf) ret = open(fsenc(ap), mode, self.args.iobuf)
if w and "chmod_f" in vfs.flags: if w and "fperms" in vfs.flags:
os.fchmod(ret.fileno(), vfs.flags["chmod_f"]) set_fperms(ret, vfs.flags)
return ret return ret
@ -279,9 +289,12 @@ class FtpFs(AbstractedFS):
# returning 550 is library-default and suitable # returning 550 is library-default and suitable
raise FSE("No such file or directory") raise FSE("No such file or directory")
avfs = vfs.chk_ap(ap, st) if vfs.realpath:
if not avfs: avfs = vfs.chk_ap(ap, st)
raise FSE("Permission denied", 1) if not avfs:
raise FSE("Permission denied", 1)
else:
avfs = vfs
self.cwd = nwd self.cwd = nwd
( (
@ -297,8 +310,7 @@ class FtpFs(AbstractedFS):
def mkdir(self, path: str) -> None: def mkdir(self, path: str) -> None:
ap, vfs, _ = self.rv2a(path, w=True) ap, vfs, _ = self.rv2a(path, w=True)
chmod = vfs.flags["chmod_d"] bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
bos.makedirs(ap, chmod) # filezilla expects this
def listdir(self, path: str) -> list[str]: def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path) vpath = join(self.cwd, path)
@ -397,8 +409,12 @@ class FtpFs(AbstractedFS):
return st return st
def utime(self, path: str, timeval: float) -> None: def utime(self, path: str, timeval: float) -> None:
ap = self.rv2a(path, w=True)[0] try:
return bos.utime(ap, (timeval, timeval)) ap = self.rv2a(path, w=True)[0]
return bos.utime(ap, (int(time.time()), int(timeval)))
except Exception as ex:
logging.error("ftp.utime: %s, %r", ex, ex)
raise
def lstat(self, path: str) -> os.stat_result: def lstat(self, path: str) -> os.stat_result:
ap = self.rv2a(path)[0] ap = self.rv2a(path)[0]
@ -487,7 +503,11 @@ class FtpHandler(FTPHandler):
def ftp_STOR(self, file: str, mode: str = "w") -> Any: def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str] # Optional[str]
vp = join(self.fs.cwd, file).lstrip("/") vp = join(self.fs.cwd, file).lstrip("/")
ap, vfs, rem = self.fs.v2a(vp, w=True) try:
ap, vfs, rem = self.fs.v2a(vp, w=True)
except Exception as ex:
self.respond("550 %s" % (ex,), logging.info)
return
self.vfs_map[ap] = vp self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu") xbu = vfs.flags.get("xbu")
if xbu and not runhook( if xbu and not runhook(
@ -607,7 +627,7 @@ class Ftpd(object):
if "::" in ips: if "::" in ips:
ips.append("0.0.0.0") ips.append("0.0.0.0")
ips = [x for x in ips if "unix:" not in x] ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
if self.args.ftp4: if self.args.ftp4:
ips = [x for x in ips if ":" not in x] ips = [x for x in ips if ":" not in x]

View file

@ -33,7 +33,7 @@ except:
from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION from .__version__ import S_VERSION
from .authsrv import VFS # typechk from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos from .bos import bos
from .star import StreamTar from .star import StreamTar
from .stolen.qrcodegen import QrCode, qr2svg from .stolen.qrcodegen import QrCode, qr2svg
@ -62,6 +62,7 @@ from .util import (
alltrace, alltrace,
atomic_move, atomic_move,
b64dec, b64dec,
eol_conv,
exclude_dotfiles, exclude_dotfiles,
formatdate, formatdate,
fsenc, fsenc,
@ -79,8 +80,10 @@ from .util import (
hidedir, hidedir,
html_bescape, html_bescape,
html_escape, html_escape,
html_sh_esc,
humansize, humansize,
ipnorm, ipnorm,
json_hesc,
justcopy, justcopy,
load_resource, load_resource,
loadpy, loadpy,
@ -103,7 +106,9 @@ from .util import (
sanitize_vpath, sanitize_vpath,
sendfile_kern, sendfile_kern,
sendfile_py, sendfile_py,
set_fperms,
stat_resource, stat_resource,
str_anchor,
ub64dec, ub64dec,
ub64enc, ub64enc,
ujoin, ujoin,
@ -258,7 +263,8 @@ class HttpCli(object):
def _assert_safe_rem(self, rem: str) -> None: def _assert_safe_rem(self, rem: str) -> None:
# sanity check to prevent any disasters # sanity check to prevent any disasters
if rem.startswith("/") or rem.startswith("../") or "/../" in rem: # (this function hopefully serves no purpose; validation has already happened at this point, this only exists as a last-ditch effort just in case)
if rem.startswith(("/", "../")) or "/../" in rem:
raise Exception("that was close") raise Exception("that was close")
def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str: def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
@ -379,9 +385,20 @@ class HttpCli(object):
try: try:
cli_ip = zsl[n].strip() cli_ip = zsl[n].strip()
except: except:
cli_ip = zsl[0].strip() cli_ip = self.ip
t = "rproxy={} oob x-fwd {}" self.bad_xff = True
self.log(t.format(self.args.rproxy, zso), c=3) if self.args.rproxy != 9999999:
t = "global-option --rproxy %d could not be used (out-of-bounds) for the received header [%s]"
self.log(t % (self.args.rproxy, zso), c=3)
else:
zsl = [
" rproxy: %d if this client's IP-address is [%s]"
% (-1 - zd, zs.strip())
for zd, zs in enumerate(zsl)
]
t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:'
t = t % (self.args.xff_hdr,)
self.log("%s\n\n%s\n" % (t, "\n".join(zsl)), 3)
pip = self.conn.addr[0] pip = self.conn.addr[0]
xffs = self.conn.xff_nm xffs = self.conn.xff_nm
@ -545,7 +562,7 @@ class HttpCli(object):
zso = self.headers.get("cookie") zso = self.headers.get("cookie")
if zso: if zso:
if len(zso) > 8192: if len(zso) > self.args.cookie_cmax:
self.loud_reply("cookie header too big", status=400) self.loud_reply("cookie header too big", status=400)
return False return False
zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x] zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
@ -553,11 +570,15 @@ class HttpCli(object):
cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or "" cookie_pw = cookies.get("cppws") or cookies.get("cppwd") or ""
if "b" in cookies and "b" not in uparam: if "b" in cookies and "b" not in uparam:
uparam["b"] = cookies["b"] uparam["b"] = cookies["b"]
if len(cookies) > self.args.cookie_nmax:
self.loud_reply("too many cookies", status=400)
else: else:
cookies = {} cookies = {}
cookie_pw = "" cookie_pw = ""
if len(uparam) > 10 or len(cookies) > 50: if len(uparam) > 12:
t = "http-request rejected; num.params: %d %r"
self.log(t % (len(uparam), self.req), 3)
self.loud_reply("u wot m8", status=400) self.loud_reply("u wot m8", status=400)
return False return False
@ -603,8 +624,22 @@ class HttpCli(object):
or "*" or "*"
) )
if self.args.idp_h_usr: if self.args.have_idp_hdrs:
idp_usr = self.headers.get(self.args.idp_h_usr) or "" idp_usr = ""
if self.args.idp_hm_usr:
for hn, hmv in self.args.idp_hm_usr_p.items():
zs = self.headers.get(hn)
if zs:
for zs1, zs2 in hmv.items():
if zs == zs1:
idp_usr = zs2
break
if idp_usr:
break
for hn in self.args.idp_h_usr:
if idp_usr:
break
idp_usr = self.headers.get(hn)
if idp_usr: if idp_usr:
idp_grp = ( idp_grp = (
self.headers.get(self.args.idp_h_grp) or "" self.headers.get(self.args.idp_h_grp) or ""
@ -622,6 +657,9 @@ class HttpCli(object):
) or self.args.idp_h_key in self.headers ) or self.args.idp_h_key in self.headers
if trusted_key and trusted_xff: if trusted_key and trusted_xff:
if idp_usr.lower() == LEELOO_DALLAS:
self.loud_reply("send her back", status=403)
return False
self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp) self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp)
else: else:
if not trusted_key: if not trusted_key:
@ -651,11 +689,20 @@ class HttpCli(object):
self.pw = "" self.pw = ""
self.uname = idp_usr self.uname = idp_usr
self.html_head += "<script>var is_idp=1</script>\n" self.html_head += "<script>var is_idp=1</script>\n"
zs = self.asrv.ases.get(idp_usr)
if zs:
self.set_idp_cookie(zs)
else: else:
self.log("unknown username: %r" % (idp_usr,), 1) self.log("unknown username: %r" % (idp_usr,), 1)
if self.args.ipu and self.uname == "*": if self.args.have_ipu_or_ipr:
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)] if self.args.ipu and self.uname == "*":
self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
ipr = self.conn.hsrv.ipr
if ipr and self.uname in ipr:
if not ipr[self.uname].map(self.ip):
self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
self.uname = "*"
self.rvol = self.asrv.vfs.aread[self.uname] self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname] self.wvol = self.asrv.vfs.awrite[self.uname]
@ -677,7 +724,7 @@ class HttpCli(object):
cookies["b"] = "" cookies["b"] = ""
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
if "xdev" in vn.flags or "xvol" in vn.flags: if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags):
ap = vn.canonical(rem) ap = vn.canonical(rem)
avn = vn.chk_ap(ap) avn = vn.chk_ap(ap)
else: else:
@ -905,7 +952,7 @@ class HttpCli(object):
if status == 304: if status == 304:
self.out_headers.pop("Content-Length", None) self.out_headers.pop("Content-Length", None)
self.out_headers.pop("Content-Type", None) self.out_headers.pop("Content-Type", None)
self.out_headerlist.clear() self.out_headerlist[:] = []
if self.k304(): if self.k304():
self.keepalive = False self.keepalive = False
else: else:
@ -1110,15 +1157,18 @@ class HttpCli(object):
else: else:
return True return True
host = self.host.lower()
if host.startswith("["):
if "]:" in host:
host = host.split("]:")[0] + "]"
else:
host = host.split(":")[0]
oh = self.out_headers oh = self.out_headers
origin = origin.lower() origin = origin.lower()
good_origins = self.args.acao + [ proto = "https" if self.is_https else "http"
"%s://%s" good_origins = self.args.acao + ["%s://%s" % (proto, host)]
% (
"https" if self.is_https else "http",
self.host.lower().split(":")[0],
)
]
if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins: if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins:
good_origin = True good_origin = True
bad_hdrs = ("",) bad_hdrs = ("",)
@ -1186,15 +1236,6 @@ class HttpCli(object):
self.reply(b"ssdp is disabled in server config", 404) self.reply(b"ssdp is disabled in server config", 404)
return False return False
if self.vpath.startswith(".cpr/dd/") and self.args.mpmc:
if self.args.mpmc == ".":
raise Pebkac(404)
loc = self.args.mpmc.rstrip("/") + self.vpath[self.vpath.rfind("/") :]
h = {"Location": loc, "Cache-Control": "max-age=39"}
self.reply(b"", 301, headers=h)
return True
if self.vpath == ".cpr/metrics": if self.vpath == ".cpr/metrics":
return self.conn.hsrv.metrics.tx(self) return self.conn.hsrv.metrics.tx(self)
@ -1575,6 +1616,22 @@ class HttpCli(object):
self.log("inaccessible: %r" % ("/" + self.vpath,)) self.log("inaccessible: %r" % ("/" + self.vpath,))
raise Pebkac(401, "authenticate") raise Pebkac(401, "authenticate")
if "quota-available-bytes" in props and not self.args.nid:
bfree, btot, _ = get_df(vn.realpath, False)
if btot:
df = {
"quota-available-bytes": str(bfree),
"quota-used-bytes": str(btot - bfree),
}
if "quotaused" in props: # macos finder crazytalk
df["quotaused"] = df["quota-used-bytes"]
if "quota" in props:
df["quota"] = df["quota-available-bytes"] # idk, makes it happy
else:
df = {}
else:
df = {}
fgen = itertools.chain([topdir], fgen) fgen = itertools.chain([topdir], fgen)
vtop = vjoin(self.args.R, vjoin(vn.vpath, rem)) vtop = vjoin(self.args.R, vjoin(vn.vpath, rem))
@ -1617,6 +1674,9 @@ class HttpCli(object):
ap = os.path.join(tap, x["vp"]) ap = os.path.join(tap, x["vp"])
pvs["getcontenttype"] = html_escape(guess_mime(rp, ap)) pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
pvs["getcontentlength"] = str(st.st_size) pvs["getcontentlength"] = str(st.st_size)
elif df:
pvs.update(df)
df = {}
for k, v in pvs.items(): for k, v in pvs.items():
if k not in props: if k not in props:
@ -1951,6 +2011,9 @@ class HttpCli(object):
if "eshare" in self.uparam: if "eshare" in self.uparam:
return self.handle_eshare() return self.handle_eshare()
if "fs_abrt" in self.uparam:
return self.handle_fs_abrt()
if "application/octet-stream" in ctype: if "application/octet-stream" in ctype:
return self.handle_post_binary() return self.handle_post_binary()
@ -2058,17 +2121,17 @@ class HttpCli(object):
rnd, lifetime, xbu, xau = self.upload_flags(vfs) rnd, lifetime, xbu, xau = self.upload_flags(vfs)
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
fdir = vfs.canonical(rem) fdir = vfs.canonical(rem)
if lim:
fdir, rem = lim.all(
self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
)
fn = None fn = None
if rem and not self.trailing_slash and not bos.path.isdir(fdir): if rem and not self.trailing_slash and not bos.path.isdir(fdir):
fdir, fn = os.path.split(fdir) fdir, fn = os.path.split(fdir)
rem, _ = vsplit(rem) rem, _ = vsplit(rem)
bos.makedirs(fdir, vfs.flags["chmod_d"]) if lim:
fdir, rem = lim.all(
self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
)
bos.makedirs(fdir, vf=vfs.flags)
open_ka: dict[str, Any] = {"fun": open} open_ka: dict[str, Any] = {"fun": open}
open_a = ["wb", self.args.iobuf] open_a = ["wb", self.args.iobuf]
@ -2126,9 +2189,7 @@ class HttpCli(object):
if nameless: if nameless:
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip()) fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
params = {"suffix": suffix, "fdir": fdir} params = {"suffix": suffix, "fdir": fdir, "vf": vfs.flags}
if "chmod_f" in vfs.flags:
params["chmod"] = vfs.flags["chmod_f"]
if self.args.nw: if self.args.nw:
params = {} params = {}
fn = os.devnull fn = os.devnull
@ -2177,7 +2238,7 @@ class HttpCli(object):
if self.args.nw: if self.args.nw:
fn = os.devnull fn = os.devnull
else: else:
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
path = os.path.join(fdir, fn) path = os.path.join(fdir, fn)
if not nameless: if not nameless:
self.vpath = vjoin(self.vpath, fn) self.vpath = vjoin(self.vpath, fn)
@ -2309,7 +2370,7 @@ class HttpCli(object):
if self.args.hook_v: if self.args.hook_v:
log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem) log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
fdir, self.vpath, fn, (vfs, rem) = x fdir, self.vpath, fn, (vfs, rem) = x
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
path2 = os.path.join(fdir, fn) path2 = os.path.join(fdir, fn)
atomic_move(self.log, path, path2, vfs.flags) atomic_move(self.log, path, path2, vfs.flags)
path = path2 path = path2
@ -2595,7 +2656,7 @@ class HttpCli(object):
dst = vfs.canonical(rem) dst = vfs.canonical(rem)
try: try:
if not bos.path.isdir(dst): if not bos.path.isdir(dst):
bos.makedirs(dst, vfs.flags["chmod_d"]) bos.makedirs(dst, vf=vfs.flags)
except OSError as ex: except OSError as ex:
self.log("makedirs failed %r" % (dst,)) self.log("makedirs failed %r" % (dst,))
if not bos.path.isdir(dst): if not bos.path.isdir(dst):
@ -2907,23 +2968,37 @@ class HttpCli(object):
def handle_chpw(self) -> bool: def handle_chpw(self) -> bool:
assert self.parser # !rm assert self.parser # !rm
if self.args.usernames:
self.parser.require("uname", 64)
pwd = self.parser.require("pw", 64) pwd = self.parser.require("pw", 64)
self.parser.drop() self.parser.drop()
ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd) ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
if ok: if ok:
self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
if self.args.usernames:
pwd = "%s:%s" % (self.uname, pwd)
ok, msg = self.get_pwd_cookie(pwd) ok, msg = self.get_pwd_cookie(pwd)
if ok: if ok:
msg = "new password OK" msg = "new password OK"
redir = (self.args.SRS + "?h") if ok else "" redir = (self.args.SRS + "?h") if ok else ""
h2 = '<a href="' + self.args.SRS + '?h">ack</a>' h2 = '<a href="' + self.args.SRS + '?h">continue</a>'
html = self.j2s("msg", h1=msg, h2=h2, redir=redir) html = self.j2s("msg", h1=msg, h2=h2, redir=redir)
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
return True return True
def handle_login(self) -> bool: def handle_login(self) -> bool:
assert self.parser # !rm assert self.parser # !rm
if self.args.usernames and not (
self.args.shr and self.vpath.startswith(self.args.shr1)
):
try:
un = self.parser.require("uname", 64)
except:
un = ""
else:
un = ""
pwd = self.parser.require("cppwd", 64) pwd = self.parser.require("cppwd", 64)
try: try:
uhash = self.parser.require("uhash", 256) uhash = self.parser.require("uhash", 256)
@ -2934,6 +3009,9 @@ class HttpCli(object):
if not pwd: if not pwd:
raise Pebkac(422, "password cannot be blank") raise Pebkac(422, "password cannot be blank")
if un:
pwd = "%s:%s" % (un, pwd)
dst = self.args.SRS dst = self.args.SRS
if self.vpath: if self.vpath:
dst += quotep(self.vpaths) dst += quotep(self.vpaths)
@ -2946,7 +3024,8 @@ class HttpCli(object):
dst += "_=1#" + html_escape(uhash, True, True) dst += "_=1#" + html_escape(uhash, True, True)
_, msg = self.get_pwd_cookie(pwd) _, msg = self.get_pwd_cookie(pwd)
html = self.j2s("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst) h2 = '<a href="' + dst + '">continue</a>'
html = self.j2s("msg", h1=msg, h2=h2, redir=dst)
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
return True return True
@ -2960,7 +3039,7 @@ class HttpCli(object):
self.get_pwd_cookie("x") self.get_pwd_cookie("x")
dst = self.args.SRS + "?h" dst = self.args.SRS + "?h"
h2 = '<a href="' + dst + '">ack</a>' h2 = '<a href="' + dst + '">continue</a>'
html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst) html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
return True return True
@ -2995,16 +3074,37 @@ class HttpCli(object):
# reset both plaintext and tls # reset both plaintext and tls
# (only affects active tls cookies when tls) # (only affects active tls cookies when tls)
for k in ("cppwd", "cppws") if self.is_https else ("cppwd",): for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
ck = gencookie(k, pwd, self.args.R, False) ck = gencookie(k, pwd, self.args.R, self.args.cookie_lax, False)
self.out_headerlist.append(("Set-Cookie", ck)) self.out_headerlist.append(("Set-Cookie", ck))
self.out_headers.pop("Set-Cookie", None) # drop keepalive self.out_headers.pop("Set-Cookie", None) # drop keepalive
else: else:
k = "cppws" if self.is_https else "cppwd" k = "cppws" if self.is_https else "cppwd"
ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly") ck = gencookie(
k,
pwd,
self.args.R,
self.args.cookie_lax,
self.is_https,
dur,
"; HttpOnly",
)
self.out_headers["Set-Cookie"] = ck self.out_headers["Set-Cookie"] = ck
return dur > 0, msg return dur > 0, msg
def set_idp_cookie(self, ases) -> None:
k = "cppws" if self.is_https else "cppwd"
ck = gencookie(
k,
ases,
self.args.R,
self.args.cookie_lax,
self.is_https,
self.args.idp_cookie,
"; HttpOnly",
)
self.out_headers["Set-Cookie"] = ck
def handle_mkdir(self) -> bool: def handle_mkdir(self) -> bool:
assert self.parser # !rm assert self.parser # !rm
new_dir = self.parser.require("name", 512) new_dir = self.parser.require("name", 512)
@ -3017,6 +3117,9 @@ class HttpCli(object):
self.gctx = vpath self.gctx = vpath
vpath = undot(vpath) vpath = undot(vpath)
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
if "nosub" in vfs.flags:
raise Pebkac(403, "mkdir is forbidden below this folder")
rem = sanitize_vpath(rem, "/") rem = sanitize_vpath(rem, "/")
fn = vfs.canonical(rem) fn = vfs.canonical(rem)
@ -3030,7 +3133,7 @@ class HttpCli(object):
raise Pebkac(405, 'folder "/%s" already exists' % (vpath,)) raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
try: try:
bos.makedirs(fn, vfs.flags["chmod_d"]) bos.makedirs(fn, vf=vfs.flags)
except OSError as ex: except OSError as ex:
if ex.errno == errno.EACCES: if ex.errno == errno.EACCES:
raise Pebkac(500, "the server OS denied write-access") raise Pebkac(500, "the server OS denied write-access")
@ -3072,8 +3175,22 @@ class HttpCli(object):
with open(fsenc(fn), "wb") as f: with open(fsenc(fn), "wb") as f:
f.write(b"`GRUNNUR`\n") f.write(b"`GRUNNUR`\n")
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
dbv, vrem = vfs.get_dbv(rem)
self.conn.hsrv.broker.say(
"up2k.hash_file",
dbv.realpath,
dbv.vpath,
dbv.flags,
vrem,
sanitized,
self.ip,
bos.stat(fn).st_mtime,
self.uname,
True,
)
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.redirect(vpath, "?edit") self.redirect(vpath, "?edit")
@ -3147,7 +3264,7 @@ class HttpCli(object):
) )
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/") upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
if not nullwrite: if not nullwrite:
bos.makedirs(fdir_base, vfs.flags["chmod_d"]) bos.makedirs(fdir_base, vf=vfs.flags)
rnd, lifetime, xbu, xau = self.upload_flags(vfs) rnd, lifetime, xbu, xau = self.upload_flags(vfs)
zs = self.uparam.get("want") or self.headers.get("accept") or "" zs = self.uparam.get("want") or self.headers.get("accept") or ""
@ -3180,7 +3297,7 @@ class HttpCli(object):
if rnd: if rnd:
fname = rand_name(fdir, fname, rnd) fname = rand_name(fdir, fname, rnd)
open_args = {"fdir": fdir, "suffix": suffix} open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
if "replace" in self.uparam: if "replace" in self.uparam:
if not self.can_delete: if not self.can_delete:
@ -3242,11 +3359,8 @@ class HttpCli(object):
else: else:
open_args["fdir"] = fdir open_args["fdir"] = fdir
if "chmod_f" in vfs.flags:
open_args["chmod"] = vfs.flags["chmod_f"]
if p_file and not nullwrite: if p_file and not nullwrite:
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
# reserve destination filename # reserve destination filename
f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix) f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
@ -3350,7 +3464,7 @@ class HttpCli(object):
if nullwrite: if nullwrite:
fdir = ap2 = "" fdir = ap2 = ""
else: else:
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
atomic_move(self.log, abspath, ap2, vfs.flags) atomic_move(self.log, abspath, ap2, vfs.flags)
abspath = ap2 abspath = ap2
sz = bos.path.getsize(abspath) sz = bos.path.getsize(abspath)
@ -3471,8 +3585,8 @@ class HttpCli(object):
ft = "{}:{}".format(self.ip, self.addr[1]) ft = "{}:{}".format(self.ip, self.addr[1])
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg) ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
f.write(ft.encode("utf-8")) f.write(ft.encode("utf-8"))
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
except Exception as ex: except Exception as ex:
suf = "\nfailed to write the upload report: {}".format(ex) suf = "\nfailed to write the upload report: {}".format(ex)
@ -3523,13 +3637,13 @@ class HttpCli(object):
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
if lim: if lim:
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker) fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
bos.makedirs(fp, vfs.flags["chmod_d"]) bos.makedirs(fp, vf=vfs.flags)
fp = os.path.join(fp, fn) fp = os.path.join(fp, fn)
rem = "{}/{}".format(rp, fn).strip("/") rem = "{}/{}".format(rp, fn).strip("/")
dbv, vrem = vfs.get_dbv(rem) dbv, vrem = vfs.get_dbv(rem)
if not rem.endswith(".md") and not self.can_delete: if not rem.lower().endswith(".md") and not self.can_delete:
raise Pebkac(400, "only markdown pls") raise Pebkac(400, "only markdown pls")
if nullwrite: if nullwrite:
@ -3591,15 +3705,17 @@ class HttpCli(object):
zs = ub64enc(zb).decode("ascii")[:24].lower() zs = ub64enc(zb).decode("ascii")[:24].lower()
dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs) dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs)
self.log("moving old version to %s/%s" % (dp, mfile2)) self.log("moving old version to %s/%s" % (dp, mfile2))
if bos.makedirs(dp, vfs.flags["chmod_d"]): if bos.makedirs(dp, vf=vfs.flags):
with open(os.path.join(dp, "dir.txt"), "wb") as f: with open(os.path.join(dp, "dir.txt"), "wb") as f:
f.write(afsenc(vrd)) f.write(afsenc(vrd))
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
elif hist_cfg == "s": elif hist_cfg == "s":
dp = os.path.join(mdir, ".hist") dp = os.path.join(mdir, ".hist")
try: try:
bos.mkdir(dp, vfs.flags["chmod_d"]) bos.mkdir(dp, vfs.flags["chmod_d"])
if "chown" in vfs.flags:
bos.chown(dp, vfs.flags["uid"], vfs.flags["gid"])
hidedir(dp) hidedir(dp)
except: except:
pass pass
@ -3611,6 +3727,9 @@ class HttpCli(object):
if p_field != "body": if p_field != "body":
raise Pebkac(400, "expected body, got {}".format(p_field)) raise Pebkac(400, "expected body, got {}".format(p_field))
if "txt_eol" in vfs.flags:
p_data = eol_conv(p_data, vfs.flags["txt_eol"])
xbu = vfs.flags.get("xbu") xbu = vfs.flags.get("xbu")
if xbu: if xbu:
if not runhook( if not runhook(
@ -3638,8 +3757,8 @@ class HttpCli(object):
wunlink(self.log, fp, vfs.flags) wunlink(self.log, fp, vfs.flags)
with open(fsenc(fp), "wb", self.args.iobuf) as f: with open(fsenc(fp), "wb", self.args.iobuf) as f:
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp) sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)
if lim: if lim:
@ -4581,7 +4700,9 @@ class HttpCli(object):
else: else:
fn = self.host.split(":")[0] fn = self.host.split(":")[0]
if vn.flags.get("zipmax") and (not self.uname or not "zipmaxu" in vn.flags): if vn.flags.get("zipmax") and not (
vn.flags.get("zipmaxu") and self.uname != "*"
):
maxs = vn.flags.get("zipmaxs_v") or 0 maxs = vn.flags.get("zipmaxs_v") or 0
maxn = vn.flags.get("zipmaxn_v") or 0 maxn = vn.flags.get("zipmaxn_v") or 0
nf = 0 nf = 0
@ -4635,7 +4756,7 @@ class HttpCli(object):
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]})) # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
cfmt = "" cfmt = ""
if self.thumbcli and not self.args.no_bacode: if self.thumbcli and not self.args.no_bacode:
for zs in ("opus", "mp3", "w", "j", "p"): for zs in ("opus", "mp3", "flac", "wav", "w", "j", "p"):
if zs in self.ouparam or uarg == zs: if zs in self.ouparam or uarg == zs:
cfmt = zs cfmt = zs
@ -4865,18 +4986,23 @@ class HttpCli(object):
def tx_svcs(self) -> bool: def tx_svcs(self) -> bool:
aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a" aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a"
ep = self.host ep = self.host
host = ep.split(":")[0] sep = "]:" if "]" in ep else ":"
hport = ep[ep.find(":") :] if ":" in ep else "" if sep in ep:
rip = ( host, hport = ep.rsplit(":", 1)
host hport = ":" + hport
if self.args.rclone_mdns or not self.args.zm else:
else self.conn.hsrv.nm.map(self.ip) or host host = ep
) hport = ""
# safer than html_escape/quotep since this avoids both XSS and shell-stuff
pw = re.sub(r"[<>&$?`\"']", "_", self.pw or "hunter2") if host.endswith(".local") and self.args.zm and not self.args.rclone_mdns:
vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/") rip = self.conn.hsrv.nm.map(self.ip) or host
pw = pw.replace(" ", "%20") if ":" in rip and "[" not in rip:
vp = vp.replace(" ", "%20") rip = "[%s]" % (rip,)
else:
rip = host
vp = (self.uparam["hc"] or "").lstrip("/")
pw = self.ouparam.get("pw") or "hunter2"
if pw in self.asrv.sesa: if pw in self.asrv.sesa:
pw = "hunter2" pw = "hunter2"
@ -4885,14 +5011,14 @@ class HttpCli(object):
args=self.args, args=self.args,
accs=bool(self.asrv.acct), accs=bool(self.asrv.acct),
s="s" if self.is_https else "", s="s" if self.is_https else "",
rip=rip, rip=html_sh_esc(rip),
ep=ep, ep=html_sh_esc(ep),
vp=vp, vp=html_sh_esc(vp),
rvp=vjoin(self.args.R, vp), rvp=html_sh_esc(vjoin(self.args.R, vp)),
host=host, host=html_sh_esc(host),
hport=hport, hport=html_sh_esc(hport),
aname=aname, aname=aname,
pw=pw, pw=html_sh_esc(pw),
) )
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
return True return True
@ -4970,7 +5096,7 @@ class HttpCli(object):
wvol = [x for x in wvol if "unlistcw" not in allvols[x[1:-1]].flags] wvol = [x for x in wvol if "unlistcw" not in allvols[x[1:-1]].flags]
fmt = self.uparam.get("ls", "") fmt = self.uparam.get("ls", "")
if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")): if not fmt and self.ua.startswith(("curl/", "fetch")):
fmt = "v" fmt = "v"
if fmt in ["v", "t", "txt"]: if fmt in ["v", "t", "txt"]:
@ -5010,6 +5136,13 @@ class HttpCli(object):
self.reply(zb, mime="text/plain; charset=utf-8") self.reply(zb, mime="text/plain; charset=utf-8")
return True return True
re_btn = ""
nre = self.args.ctl_re
if "re" in self.uparam:
self.out_headers["Refresh"] = str(nre)
elif nre:
re_btn = "&re=%s" % (nre,)
html = self.j2s( html = self.j2s(
"splash", "splash",
this=self, this=self,
@ -5027,6 +5160,7 @@ class HttpCli(object):
mtpq=vs["mtpq"], mtpq=vs["mtpq"],
dbwt=vs["dbwt"], dbwt=vs["dbwt"],
url_suf=suf, url_suf=suf,
re=re_btn,
k304=self.k304(), k304=self.k304(),
no304=self.no304(), no304=self.no304(),
k304vis=self.args.k304 > 0, k304vis=self.args.k304 > 0,
@ -5041,7 +5175,7 @@ class HttpCli(object):
def setck(self) -> bool: def setck(self) -> bool:
k, v = self.uparam["setck"].split("=", 1) k, v = self.uparam["setck"].split("=", 1)
t = 0 if v in ("", "x") else 86400 * 299 t = 0 if v in ("", "x") else 86400 * 299
ck = gencookie(k, v, self.args.R, False, t) ck = gencookie(k, v, self.args.R, self.args.cookie_lax, False, t)
self.out_headerlist.append(("Set-Cookie", ck)) self.out_headerlist.append(("Set-Cookie", ck))
if "cc" in self.ouparam: if "cc" in self.ouparam:
self.redirect("", "?h#cc") self.redirect("", "?h#cc")
@ -5053,7 +5187,7 @@ class HttpCli(object):
for k in ALL_COOKIES: for k in ALL_COOKIES:
if k not in self.cookies: if k not in self.cookies:
continue continue
cookie = gencookie(k, "x", self.args.R, False) cookie = gencookie(k, "x", self.args.R, self.args.cookie_lax, False)
self.out_headerlist.append(("Set-Cookie", cookie)) self.out_headerlist.append(("Set-Cookie", cookie))
self.redirect("", "?h#cc") self.redirect("", "?h#cc")
@ -5072,7 +5206,7 @@ class HttpCli(object):
t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id="r" href="{}/?h">go home</a></p>' t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p><a id="r" href="{}/?h">go home</a></p>'
pt = "404 not found ┐( ´ -`)┌" pt = "404 not found ┐( ´ -`)┌"
if self.ua.startswith("curl/") or self.ua.startswith("fetch"): if self.ua.startswith(("curl/", "fetch")):
pt = "# acct: %s\n%s\n" % (self.uname, pt) pt = "# acct: %s\n%s\n" % (self.uname, pt)
self.reply(pt.encode("utf-8"), status=rc) self.reply(pt.encode("utf-8"), status=rc)
return True return True
@ -5316,15 +5450,16 @@ class HttpCli(object):
raise Pebkac(500, "sqlite3 not found on server; unpost is disabled") raise Pebkac(500, "sqlite3 not found on server; unpost is disabled")
raise Pebkac(500, "server busy, cannot unpost; please retry in a bit") raise Pebkac(500, "server busy, cannot unpost; please retry in a bit")
zs = self.uparam.get("filter") or "" sfilt = self.uparam.get("filter") or ""
filt = re.compile(zs, re.I) if zs else None nfi, vfi = str_anchor(sfilt)
lm = "ups %r" % (zs,) lm = "ups %d%r" % (nfi, sfilt)
if self.args.shr and self.vpath.startswith(self.args.shr1): if self.args.shr and self.vpath.startswith(self.args.shr1):
shr_dbv, shr_vrem = self.vn.get_dbv(self.rem) shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
else: else:
shr_dbv = None shr_dbv = None
wret: dict[str, Any] = {}
ret: list[dict[str, Any]] = [] ret: list[dict[str, Any]] = []
t0 = time.time() t0 = time.time()
lim = time.time() - self.args.unpost lim = time.time() - self.args.unpost
@ -5346,7 +5481,13 @@ class HttpCli(object):
x = self.conn.hsrv.broker.ask( x = self.conn.hsrv.broker.ask(
"up2k.get_unfinished_by_user", self.uname, "" if bad_xff else self.ip "up2k.get_unfinished_by_user", self.uname, "" if bad_xff else self.ip
) )
uret = x.get() zdsa: dict[str, Any] = x.get()
uret: list[dict[str, Any]] = []
if "timeout" in zdsa:
wret["nou"] = 1
else:
uret = zdsa["f"]
nu = len(uret)
if not self.args.unpost: if not self.args.unpost:
allvols = [] allvols = []
@ -5360,6 +5501,10 @@ class HttpCli(object):
and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv) and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
] ]
q = ""
qp = (0,)
q_c = -1
for vol in allvols: for vol in allvols:
cur = idx.get_cur(vol) cur = idx.get_cur(vol)
if not cur: if not cur:
@ -5367,11 +5512,33 @@ class HttpCli(object):
nfk, fk_alg = fk_vols.get(vol) or (0, 0) nfk, fk_alg = fk_vols.get(vol) or (0, 0)
zi = vol.flags["unp_who"]
if q_c != zi:
q_c = zi
q = "select sz, rd, fn, at from up where "
if zi == 1:
q += "ip=? and un=?"
qp = (self.ip, self.uname, lim)
elif zi == 2:
q += "ip=?"
qp = (self.ip, lim)
if zi == 3:
q += "un=?"
qp = (self.uname, lim)
q += " and at>? order by at desc"
n = 2000 n = 2000
q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc" for sz, rd, fn, at in cur.execute(q, qp):
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if filt and not filt.search(vp): if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
pass
elif nfi == 2:
if not vp.lower().startswith(vfi):
continue
elif nfi == 3:
if not vp.lower().endswith(vfi):
continue
else:
continue continue
n -= 1 n -= 1
@ -5392,6 +5559,8 @@ class HttpCli(object):
if len(ret) > 2000: if len(ret) > 2000:
ret = ret[:2000] ret = ret[:2000]
if len(ret) >= 2000:
wret["oc"] = 1
for rv in ret: for rv in ret:
rv["vp"] = quotep(rv["vp"]) rv["vp"] = quotep(rv["vp"])
@ -5411,6 +5580,13 @@ class HttpCli(object):
) )
rv["vp"] += "?k=" + fk[:nfk] rv["vp"] += "?k=" + fk[:nfk]
if not allvols:
wret["noc"] = 1
ret = []
nc = len(ret)
ret = uret + ret
if shr_dbv: if shr_dbv:
# translate vpaths from share-target to share-url # translate vpaths from share-target to share-url
# to satisfy access checks # to satisfy access checks
@ -5425,12 +5601,11 @@ class HttpCli(object):
for v in ret: for v in ret:
v["vp"] = self.args.SR + v["vp"] v["vp"] = self.args.SR + v["vp"]
if not allvols: wret["f"] = ret
ret = [{"kinshi": 1}] wret["nu"] = nu
wret["nc"] = nc
jtxt = '{"u":%s,"c":%s}' % (uret, json.dumps(ret, separators=(",\n", ": "))) jtxt = json.dumps(wret, separators=(",\n", ": "))
zi = len(uret.split('\n"pd":')) - 1 self.log("%s #%d+%d %.2fsec" % (lm, nu, nc, time.time() - t0))
self.log("%s #%d+%d %.2fsec" % (lm, zi, len(ret), time.time() - t0))
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True return True
@ -5445,8 +5620,8 @@ class HttpCli(object):
raise Pebkac(500, "server busy, cannot list recent uploads; please retry") raise Pebkac(500, "server busy, cannot list recent uploads; please retry")
sfilt = self.uparam.get("filter") or "" sfilt = self.uparam.get("filter") or ""
filt = re.compile(sfilt, re.I) if sfilt else None nfi, vfi = str_anchor(sfilt)
lm = "ru %r" % (sfilt,) lm = "ru %d%r" % (nfi, sfilt)
self.log(lm) self.log(lm)
ret: list[dict[str, Any]] = [] ret: list[dict[str, Any]] = []
@ -5478,10 +5653,18 @@ class HttpCli(object):
continue continue
n = 1000 n = 1000
q = "select sz, rd, fn, ip, at from up where at>0 order by at desc" q = "select sz, rd, fn, ip, at, un from up where at>0 order by at desc"
for sz, rd, fn, ip, at in cur.execute(q): for sz, rd, fn, ip, at, un in cur.execute(q):
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x) vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if filt and not filt.search(vp): if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
pass
elif nfi == 2:
if not vp.lower().startswith(vfi):
continue
elif nfi == 3:
if not vp.lower().endswith(vfi):
continue
else:
continue continue
if not dots and "/." in vp: if not dots and "/." in vp:
@ -5492,6 +5675,7 @@ class HttpCli(object):
"sz": sz, "sz": sz,
"ip": ip, "ip": ip,
"at": at, "at": at,
"un": un,
"nfk": nfk, "nfk": nfk,
"adm": adm, "adm": adm,
} }
@ -5536,12 +5720,16 @@ class HttpCli(object):
adm = rv.pop("adm") adm = rv.pop("adm")
if not adm: if not adm:
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)" rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
if rv["un"] not in ("*", self.uname):
rv["un"] = "(?)"
else: else:
for rv in ret: for rv in ret:
adm = rv.pop("adm") adm = rv.pop("adm")
if not adm: if not adm:
rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)" rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
rv["at"] = 0 rv["at"] = 0
if rv["un"] not in ("*", self.uname):
rv["un"] = "(?)"
if self.is_vproxied: if self.is_vproxied:
for v in ret: for v in ret:
@ -5556,7 +5744,7 @@ class HttpCli(object):
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True return True
html = self.j2s("rups", this=self, v=jtxt) html = self.j2s("rups", this=self, v=json_hesc(jtxt))
self.reply(html.encode("utf-8"), status=200) self.reply(html.encode("utf-8"), status=200)
return True return True
@ -5620,15 +5808,15 @@ class HttpCli(object):
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled") raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot create share; please retry in a bit") raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
skey = self.uparam.get("skey") or self.vpath.split("/")[-1]
if self.args.shr_v: if self.args.shr_v:
self.log("handle_eshare: " + self.req) self.log("handle_eshare: " + skey)
cur = idx.get_shr() cur = idx.get_shr()
if not cur: if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...") raise Pebkac(400, "huh, sharing must be disabled in the server config...")
skey = self.vpath.split("/")[-1]
rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall() rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
un = rows[0][0] if rows and rows[0] else "" un = rows[0][0] if rows and rows[0] else ""
@ -5820,7 +6008,9 @@ class HttpCli(object):
self.asrv.vfs.get(vdst, self.uname, False, True, False, True) self.asrv.vfs.get(vdst, self.uname, False, True, False, True)
wunlink(self.log, dabs, dvn.flags) wunlink(self.log, dabs, dvn.flags)
x = self.conn.hsrv.broker.ask("up2k.handle_mv", self.uname, self.ip, vsrc, vdst) x = self.conn.hsrv.broker.ask(
"up2k.handle_mv", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst
)
self.loud_reply(x.get(), status=201) self.loud_reply(x.get(), status=201)
return True return True
@ -5850,10 +6040,21 @@ class HttpCli(object):
self.asrv.vfs.get(vdst, self.uname, False, True, False, True) self.asrv.vfs.get(vdst, self.uname, False, True, False, True)
wunlink(self.log, dabs, dvn.flags) wunlink(self.log, dabs, dvn.flags)
x = self.conn.hsrv.broker.ask("up2k.handle_cp", self.uname, self.ip, vsrc, vdst) x = self.conn.hsrv.broker.ask(
"up2k.handle_cp", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst
)
self.loud_reply(x.get(), status=201) self.loud_reply(x.get(), status=201)
return True return True
def handle_fs_abrt(self):
if self.args.no_fs_abrt:
t = "aborting an ongoing copy/move is disabled in server config"
raise Pebkac(403, t)
self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"])
self.loud_reply("aborting", status=200)
return True
def tx_ls(self, ls: dict[str, Any]) -> bool: def tx_ls(self, ls: dict[str, Any]) -> bool:
dirs = ls["dirs"] dirs = ls["dirs"]
files = ls["files"] files = ls["files"]
@ -5913,6 +6114,12 @@ class HttpCli(object):
else: else:
[x.pop(k) for k in ["name", "dt"] for y in [dirs, files] for x in y] [x.pop(k) for k in ["name", "dt"] for y in [dirs, files] for x in y]
# nonce (tlnote: norwegian for flake as in snowflake)
if self.args.no_fnugg:
ls["fnugg"] = "nei"
elif "fnugg" in self.headers:
ls["fnugg"] = self.headers["fnugg"]
ret = json.dumps(ls) ret = json.dumps(ls)
mime = "application/json" mime = "application/json"
@ -6095,7 +6302,8 @@ class HttpCli(object):
if not use_filekey: if not use_filekey:
return self.tx_404(True) return self.tx_404(True)
if add_og and not abspath.lower().endswith(".md"): is_md = abspath.lower().endswith(".md")
if add_og and not is_md:
if og_ua or self.host not in self.headers.get("referer", ""): if og_ua or self.host not in self.headers.get("referer", ""):
self.vpath, og_fn = vsplit(self.vpath) self.vpath, og_fn = vsplit(self.vpath)
vpath = self.vpath vpath = self.vpath
@ -6107,10 +6315,10 @@ class HttpCli(object):
vpnodes.pop() vpnodes.pop()
if ( if (
(abspath.endswith(".md") or self.can_delete) (is_md or self.can_delete)
and "nohtml" not in vn.flags and "nohtml" not in vn.flags
and ( and (
("v" in self.uparam and abspath.endswith(".md")) (is_md and "v" in self.uparam)
or "edit" in self.uparam or "edit" in self.uparam
or "edit2" in self.uparam or "edit2" in self.uparam
) )
@ -6137,13 +6345,13 @@ class HttpCli(object):
self.log("#wow #whoa") self.log("#wow #whoa")
if not self.args.nid: if not self.args.nid:
free, total, _ = get_df(abspath, False) free, total, zs = get_df(abspath, False)
if total is not None: if total:
h1 = humansize(free or 0) h1 = humansize(free or 0)
h2 = humansize(total) h2 = humansize(total)
srv_info.append("{} free of {}".format(h1, h2)) srv_info.append("{} free of {}".format(h1, h2))
elif free is not None: elif zs:
srv_info.append(humansize(free, True) + " free") self.log("diskfree(%r): %s" % (abspath, zs), 3)
srv_infot = "</span> // <span>".join(srv_info) srv_infot = "</span> // <span>".join(srv_info)
@ -6167,11 +6375,7 @@ class HttpCli(object):
is_ls = "ls" in self.uparam is_ls = "ls" in self.uparam
is_js = self.args.force_js or self.cookies.get("js") == "y" is_js = self.args.force_js or self.cookies.get("js") == "y"
if ( if not is_ls and not add_og and self.ua.startswith(("curl/", "fetch")):
not is_ls
and not add_og
and (self.ua.startswith("curl/") or self.ua.startswith("fetch"))
):
self.uparam["ls"] = "v" self.uparam["ls"] = "v"
is_ls = True is_ls = True
@ -6447,13 +6651,15 @@ class HttpCli(object):
tags = {k: v for k, v in r} tags = {k: v for k, v in r}
if is_admin: if is_admin:
q = "select ip, at from up where rd=? and fn=?" q = "select ip, at, un from up where rd=? and fn=?"
try: try:
zs1, zs2 = icur.execute(q, erd_efn).fetchone() zs1, zs2, zs3 = icur.execute(q, erd_efn).fetchone()
if zs1: if zs1:
tags["up_ip"] = zs1 tags["up_ip"] = zs1
if zs2: if zs2:
tags[".up_at"] = zs2 tags[".up_at"] = zs2
if zs3:
tags["up_by"] = zs3
except: except:
pass pass
elif add_up_at: elif add_up_at:
@ -6474,7 +6680,7 @@ class HttpCli(object):
lmte = list(mte) lmte = list(mte)
if self.can_admin: if self.can_admin:
lmte.extend(("up_ip", ".up_at")) lmte.extend(("up_by", "up_ip", ".up_at"))
if "nodirsz" not in vf: if "nodirsz" not in vf:
tagset.add(".files") tagset.add(".files")

View file

@ -70,6 +70,7 @@ from .util import (
build_netmap, build_netmap,
has_resource, has_resource,
ipnorm, ipnorm,
load_ipr,
load_ipu, load_ipu,
load_resource, load_resource,
min_ex, min_ex,
@ -123,6 +124,7 @@ class HttpSrv(object):
self.nm = NetMap([], []) self.nm = NetMap([], [])
self.ssdp: Optional["SSDPr"] = None self.ssdp: Optional["SSDPr"] = None
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403) self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False) self.g422 = Garda(self.args.ban_422, False)
@ -192,6 +194,11 @@ class HttpSrv(object):
else: else:
self.ipu_iu = self.ipu_nm = None self.ipu_iu = self.ipu_nm = None
if self.args.ipr:
self.ipr = load_ipr(self.log, self.args.ipr)
else:
self.ipr = None
self.ipa_nm = build_netmap(self.args.ipa) self.ipa_nm = build_netmap(self.args.ipa)
self.xff_nm = build_netmap(self.args.xff_src) self.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan") self.xff_lan = build_netmap("lan")
@ -323,7 +330,8 @@ class HttpSrv(object):
spins = 0 spins = 0
while self.ncli >= self.nclimax: while self.ncli >= self.nclimax:
if not spins: if not spins:
self.log(self.name, "at connection limit; waiting", 3) t = "at connection limit (global-option 'nc'); waiting"
self.log(self.name, t, 3)
spins += 1 spins += 1
time.sleep(0.1) time.sleep(0.1)

View file

@ -76,7 +76,8 @@ class MDNS(MCast):
if not self.args.zm_nwa_1: if not self.args.zm_nwa_1:
set_avahi_379() set_avahi_379()
zs = self.args.name + ".local." zs = self.args.zm_fqdn or (self.args.name + ".local")
zs = zs.replace("--name", self.args.name).rstrip(".") + "."
zs = zs.encode("ascii", "replace").decode("ascii", "replace") zs = zs.encode("ascii", "replace").decode("ascii", "replace")
self.hn = "-".join(x for x in zs.split("?") if x) or ( self.hn = "-".join(x for x in zs.split("?") if x) or (
"vault-{}".format(random.randint(1, 255)) "vault-{}".format(random.randint(1, 255))

View file

@ -29,7 +29,7 @@ from .util import (
) )
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union from typing import IO, Any, Optional, Union
from .util import NamedLogger, RootLogger from .util import NamedLogger, RootLogger
@ -67,6 +67,8 @@ HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe")
CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split()) CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif".split())
CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b") CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
FMT_AU = set("mp3 ogg flac wav".split())
class MParser(object): class MParser(object):
def __init__(self, cmdline: str) -> None: def __init__(self, cmdline: str) -> None:
@ -166,12 +168,16 @@ def au_unpk(
znil = [x for x in znil if "cover" in x[0]] or znil znil = [x for x in znil if "cover" in x[0]] or znil
znil = [x for x in znil if CBZ_01.search(x[0])] or znil znil = [x for x in znil if CBZ_01.search(x[0])] or znil
t = "cbz: %d files, %d hits" % (nf, len(znil)) t = "cbz: %d files, %d hits" % (nf, len(znil))
using = sorted(znil)[0][1].filename
if znil: if znil:
t += ", using " + znil[0][1].filename t += ", using " + using
log(t) log(t)
if not znil: if not znil:
raise Exception("no images inside cbz") raise Exception("no images inside cbz")
fi = zf.open(znil[0][1]) fi = zf.open(using)
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
else: else:
raise Exception("unknown compression %s" % (pk,)) raise Exception("unknown compression %s" % (pk,))
@ -202,7 +208,7 @@ def au_unpk(
def ffprobe( def ffprobe(
abspath: str, timeout: int = 60 abspath: str, timeout: int = 60
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
cmd = [ cmd = [
b"ffprobe", b"ffprobe",
b"-hide_banner", b"-hide_banner",
@ -216,8 +222,17 @@ def ffprobe(
return parse_ffprobe(so) return parse_ffprobe(so)
def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: def parse_ffprobe(
"""ffprobe -show_format -show_streams""" txt: str,
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]:
"""
txt: output from ffprobe -show_format -show_streams
returns:
* normalized tags
* original/raw tags
* list of streams
* format props
"""
streams = [] streams = []
fmt = {} fmt = {}
g = {} g = {}
@ -241,7 +256,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
ret: dict[str, Any] = {} # processed ret: dict[str, Any] = {} # processed
md: dict[str, list[Any]] = {} # raw tags md: dict[str, list[Any]] = {} # raw tags
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"] is_audio = fmt.get("format_name") in FMT_AU
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]: if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
is_audio = True is_audio = True
@ -269,6 +284,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
["channel_layout", "chs"], ["channel_layout", "chs"],
["sample_rate", ".hz"], ["sample_rate", ".hz"],
["bit_rate", ".aq"], ["bit_rate", ".aq"],
["bits_per_sample", ".bps"],
["bits_per_raw_sample", ".bprs"],
["duration", ".dur"], ["duration", ".dur"],
] ]
@ -308,7 +325,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
ret[rk] = v1 ret[rk] = v1
if ret.get("vc") == "ansi": # shellscript if ret.get("vc") == "ansi": # shellscript
return {}, {} return {}, {}, [], {}
for strm in streams: for strm in streams:
for sk, sv in strm.items(): for sk, sv in strm.items():
@ -357,7 +374,77 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
zero = int("0") zero = int("0")
zd = {k: (zero, v) for k, v in ret.items()} zd = {k: (zero, v) for k, v in ret.items()}
return zd, md return zd, md, streams, fmt
def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]:
import zipfile
from .dxml import parse_xml
try:
from urlparse import urljoin # Python2
except ImportError:
from urllib.parse import urljoin # Python3
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())
except KeyError:
log("epub: no container file found in %s" % (abspath,))
return None
# https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem
container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"}
# One file could contain multiple package documents, default to the first one
rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get(
"full-path"
)
# Then open the first package document to find the path of the cover image
try:
package_root = parse_xml(z.read(rootfile_path).decode())
except KeyError:
log("epub: no package document found in %s" % (abspath,))
return None
# https://www.w3.org/TR/epub-33/#sec-package-doc
package_ns = {"": "http://www.idpf.org/2007/opf"}
# https://www.w3.org/TR/epub-33/#sec-cover-image
coverimage_path_node = package_root.find(
"./manifest/item[@properties='cover-image']", package_ns
)
if coverimage_path_node is not None:
coverimage_path = coverimage_path_node.get("href")
else:
# This might be an EPUB2 file, try the legacy way of specifying covers
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
# This url is either absolute (in the .epub) or relative to the package document
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
return z.open(adjusted_cover_path)
def _get_cover_from_epub2(
log: "NamedLogger", package_root, package_ns
) -> Optional[str]:
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
# <item> in <manifest>
cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
"content"
)
if not cover_id:
return None
for node in package_root.iterfind("./manifest/item", package_ns):
if node.get("id") == cover_id:
cover_path = node.get("href")
return cover_path
return None
class MTag(object): class MTag(object):
@ -628,7 +715,7 @@ class MTag(object):
if not bos.path.isfile(abspath): if not bos.path.isfile(abspath):
return {} return {}
ret, md = ffprobe(abspath, self.args.mtag_to) ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
if self.args.mtag_vv: if self.args.mtag_vv:
for zd in (ret, dict(md)): for zd in (ret, dict(md)):

View file

@ -183,11 +183,7 @@ class MCast(object):
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False) srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
# gvfs breaks if a linklocal ip appears in a dns reply # gvfs breaks if a linklocal ip appears in a dns reply
ll = { ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
k: v
for k, v in srv.ips.items()
if k.startswith("169.254") or k.startswith("fe80")
}
rt = {k: v for k, v in srv.ips.items() if k not in ll} rt = {k: v for k, v in srv.ips.items() if k not in ll}
if self.args.ll or not rt: if self.args.ll or not rt:

View file

@ -147,6 +147,10 @@ class PWHash(object):
def cli(self) -> None: def cli(self) -> None:
import getpass import getpass
if self.args.usernames:
t = "since you have enabled --usernames, please provide username:password"
print(t)
while True: while True:
try: try:
p1 = getpass.getpass("password> ") p1 = getpass.getpass("password> ")

View file

@ -320,7 +320,7 @@ class SMB(object):
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2) self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
try: try:
bos.makedirs(ap2, vfs2.flags["chmod_d"]) bos.makedirs(ap2, vf=vfs2.flags)
except: except:
pass pass

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import argparse import argparse
import atexit
import errno import errno
import logging import logging
import os import os
@ -38,6 +39,7 @@ from .th_srv import (
HAVE_FFPROBE, HAVE_FFPROBE,
HAVE_HEIF, HAVE_HEIF,
HAVE_PIL, HAVE_PIL,
HAVE_RAW,
HAVE_VIPS, HAVE_VIPS,
HAVE_WEBP, HAVE_WEBP,
ThumbSrv, ThumbSrv,
@ -51,6 +53,7 @@ from .util import (
HAVE_PSUTIL, HAVE_PSUTIL,
HAVE_SQLITE3, HAVE_SQLITE3,
HAVE_ZMQ, HAVE_ZMQ,
RE_ANSI,
URL_BUG, URL_BUG,
UTC, UTC,
VERSIONS, VERSIONS,
@ -60,10 +63,10 @@ from .util import (
HMaccas, HMaccas,
ODict, ODict,
alltrace, alltrace,
ansi_re,
build_netmap, build_netmap,
expat_ver, expat_ver,
gzip, gzip,
load_ipr,
load_ipu, load_ipu,
lock_file, lock_file,
min_ex, min_ex,
@ -72,6 +75,7 @@ from .util import (
pybin, pybin,
start_log_thrs, start_log_thrs,
start_stackmon, start_stackmon,
termsize,
ub64enc, ub64enc,
) )
@ -152,6 +156,7 @@ class SvcHub(object):
args.no_del = True args.no_del = True
args.no_mv = True args.no_mv = True
args.hardlink = True args.hardlink = True
args.dav_auth = True
args.vague_403 = True args.vague_403 = True
args.nih = True args.nih = True
@ -168,6 +173,7 @@ class SvcHub(object):
# for non-http clients (ftp, tftp) # for non-http clients (ftp, tftp)
self.bans: dict[str, int] = {} self.bans: dict[str, int] = {}
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403) self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False) self.g422 = Garda(self.args.ban_422, False)
@ -239,7 +245,7 @@ class SvcHub(object):
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s" t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
self.log("root", t % (args.th_ram_max, zs), 3) self.log("root", t % (args.th_ram_max, zs), 3)
if args.chpw and args.idp_h_usr: if args.chpw and args.have_idp_hdrs:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr" t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
self.log("root", t, 1) self.log("root", t, 1)
raise Exception(t) raise Exception(t)
@ -255,6 +261,10 @@ class SvcHub(object):
setattr(args, "ipu_iu", iu) setattr(args, "ipu_iu", iu)
setattr(args, "ipu_nm", nm) setattr(args, "ipu_nm", nm)
if args.ipr:
ipr = load_ipr(self.log, args.ipr, True)
setattr(args, "ipr_u", ipr)
for zs in "ah_salt fk_salt dk_salt".split(): for zs in "ah_salt fk_salt dk_salt".split():
if getattr(args, "show_%s" % (zs,)): if getattr(args, "show_%s" % (zs,)):
self.log("root", "effective %s is %s" % (zs, getattr(args, zs))) self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
@ -264,7 +274,7 @@ class SvcHub(object):
args.no_ses = True args.no_ses = True
args.shr = "" args.shr = ""
if args.idp_store and args.idp_h_usr: if args.idp_store and args.have_idp_hdrs:
self.setup_db("idp") self.setup_db("idp")
if not self.args.no_ses: if not self.args.no_ses:
@ -320,6 +330,8 @@ class SvcHub(object):
decs.pop("vips", None) decs.pop("vips", None)
if not HAVE_PIL: if not HAVE_PIL:
decs.pop("pil", None) decs.pop("pil", None)
if not HAVE_RAW:
decs.pop("raw", None)
if not HAVE_FFMPEG or not HAVE_FFPROBE: if not HAVE_FFMPEG or not HAVE_FFPROBE:
decs.pop("ff", None) decs.pop("ff", None)
@ -426,6 +438,9 @@ class SvcHub(object):
getattr(args, zs).mutex = threading.Lock() getattr(args, zs).mutex = threading.Lock()
except: except:
pass pass
if args.ipr:
for nm in args.ipr_u.values():
nm.mutex = threading.Lock()
def _db_onfail_ses(self) -> None: def _db_onfail_ses(self) -> None:
self.args.no_ses = True self.args.no_ses = True
@ -771,6 +786,39 @@ class SvcHub(object):
def sigterm(self) -> None: def sigterm(self) -> None:
self.signal_handler(signal.SIGTERM, None) self.signal_handler(signal.SIGTERM, None)
def sticky_qr(self) -> None:
tw, th = termsize()
zs1, qr = self.tcpsrv.qr.split("\n", 1)
url, colr = zs1.split(" ", 1)
nl = len(qr.split("\n")) # numlines
lp = 3 if nl * 2 + 4 < tw else 0 # leftpad
lp0 = lp
if self.args.qr_pin == 2:
url = ""
else:
while lp and (nl + lp) * 2 + len(url) + 1 > tw:
lp -= 1
if (nl + lp) * 2 + len(url) + 1 > tw:
qr = url + "\n" + qr
url = ""
nl += 1
lp = lp0
sh = 1 + th - nl
if lp:
zs = " " * lp
qr = zs + qr.replace("\n", "\n" + zs)
if url:
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
qr = colr + qr
def unlock():
print("\033[s\033[r\033[u", file=sys.stderr)
atexit.register(unlock)
t = "%s\033[%dA" % ("\n" * nl, nl)
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
self.pr(t, file=sys.stderr)
def cb_httpsrv_up(self) -> None: def cb_httpsrv_up(self) -> None:
self.httpsrv_up += 1 self.httpsrv_up += 1
if self.httpsrv_up != self.broker.num_workers: if self.httpsrv_up != self.broker.num_workers:
@ -783,7 +831,10 @@ class SvcHub(object):
break break
if self.tcpsrv.qr: if self.tcpsrv.qr:
self.log("qr-code", self.tcpsrv.qr) if self.args.qr_pin:
self.sticky_qr()
else:
self.log("qr-code", self.tcpsrv.qr)
else: else:
self.log("root", "workers OK\n") self.log("root", "workers OK\n")
@ -810,6 +861,7 @@ class SvcHub(object):
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"), (HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"), (HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"), (HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
(HAVE_RAW, "rawpy", "read RAW images"),
] ]
if ANYWIN: if ANYWIN:
to_check += [ to_check += [
@ -849,15 +901,6 @@ class SvcHub(object):
def _check_env(self) -> None: def _check_env(self) -> None:
al = self.args al = self.args
try:
files = os.listdir(E.cfg)
except:
files = []
hits = [x for x in files if x.lower().endswith(".conf")]
if hits:
t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
self.log("root", t % (E.cfg, ", ".join(hits)), 3)
if self.args.no_bauth: if self.args.no_bauth:
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead" t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
@ -867,7 +910,7 @@ class SvcHub(object):
have_tcp = False have_tcp = False
for zs in al.i: for zs in al.i:
if not zs.startswith("unix:"): if not zs.startswith(("unix:", "fd:")):
have_tcp = True have_tcp = True
if not have_tcp: if not have_tcp:
zb = False zb = False
@ -877,7 +920,7 @@ class SvcHub(object):
setattr(al, zs, False) setattr(al, zs, False)
zb = True zb = True
if zb: if zb:
t = "only listening on unix-sockets; cannot enable zeroconf/mdns/ssdp as requested" 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) self.log("root", t, 3)
if not self.args.no_dav: if not self.args.no_dav:
@ -977,10 +1020,23 @@ class SvcHub(object):
al.sus_urls = None al.sus_urls = None
al.xff_hdr = al.xff_hdr.lower() al.xff_hdr = al.xff_hdr.lower()
al.idp_h_usr = al.idp_h_usr.lower() al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []]
al.idp_h_grp = al.idp_h_grp.lower() al.idp_h_grp = al.idp_h_grp.lower()
al.idp_h_key = al.idp_h_key.lower() al.idp_h_key = al.idp_h_key.lower()
al.idp_hm_usr_p = {}
for zs0 in al.idp_hm_usr or []:
try:
sep = zs0[:1]
hn, zs1, zs2 = zs0[1:].split(sep)
hn = hn.lower()
if hn in al.idp_hm_usr_p:
al.idp_hm_usr_p[hn][zs1] = zs2
else:
al.idp_hm_usr_p[hn] = {zs1: zs2}
except:
raise Exception("invalid --idp-hm-usr [%s]" % (zs0,))
al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True) al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa, True)
al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True) al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
@ -1025,6 +1081,8 @@ class SvcHub(object):
except: except:
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,)) raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
al.js_utc = "false" if al.localtime else "true"
al.tcolor = al.tcolor.lstrip("#") al.tcolor = al.tcolor.lstrip("#")
if len(al.tcolor) == 3: # fc5 => ffcc55 if len(al.tcolor) == 3: # fc5 => ffcc55
al.tcolor = "".join([x * 2 for x in al.tcolor]) al.tcolor = "".join([x * 2 for x in al.tcolor])
@ -1406,11 +1464,18 @@ class SvcHub(object):
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if self.no_ansi: if self.no_ansi:
fmt = "%s %-21s %s\n" if c == 1:
fmt = "%s %-21s CRIT: %s\n"
elif c == 3:
fmt = "%s %-21s WARN: %s\n"
elif c == 6:
fmt = "%s %-21s BTW: %s\n"
else:
fmt = "%s %-21s LOG: %s\n"
if "\033" in msg: if "\033" in msg:
msg = ansi_re.sub("", msg) msg = RE_ANSI.sub("", msg)
if "\033" in src: if "\033" in src:
src = ansi_re.sub("", src) src = RE_ANSI.sub("", src)
elif c: elif c:
if isinstance(c, int): if isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg) msg = "\033[3%sm%s\033[0m" % (c, msg)

View file

@ -25,8 +25,8 @@ from .util import (
termsize, termsize,
) )
if True: if True: # pylint: disable=using-constant-test
from typing import Generator, Union from typing import Generator, Optional, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
@ -245,8 +245,10 @@ class TcpSrv(object):
def _listen(self, ip: str, port: int) -> None: def _listen(self, ip: str, port: int) -> None:
uds_perm = uds_gid = -1 uds_perm = uds_gid = -1
bound: Optional[socket.socket] = None
tcp = False
if "unix:" in ip: if "unix:" in ip:
tcp = False
ipv = socket.AF_UNIX ipv = socket.AF_UNIX
uds = ip.split(":") uds = ip.split(":")
ip = uds[-1] ip = uds[-1]
@ -259,7 +261,12 @@ class TcpSrv(object):
import grp import grp
uds_gid = grp.getgrnam(uds[2]).gr_gid uds_gid = grp.getgrnam(uds[2]).gr_gid
elif "fd:" in ip:
fd = ip[3:]
bound = socket.socket(fileno=int(fd))
tcp = bound.proto == socket.IPPROTO_TCP
ipv = bound.family
elif ":" in ip: elif ":" in ip:
tcp = True tcp = True
ipv = socket.AF_INET6 ipv = socket.AF_INET6
@ -267,7 +274,7 @@ class TcpSrv(object):
tcp = True tcp = True
ipv = socket.AF_INET ipv = socket.AF_INET
srv = socket.socket(ipv, socket.SOCK_STREAM) srv = bound or socket.socket(ipv, socket.SOCK_STREAM)
if not ANYWIN or self.args.reuseaddr: if not ANYWIN or self.args.reuseaddr:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -285,6 +292,10 @@ class TcpSrv(object):
if getattr(self.args, "freebind", False): if getattr(self.args, "freebind", False):
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1) srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
if bound:
self.srv.append(srv)
return
try: try:
if tcp: if tcp:
srv.bind((ip, port)) srv.bind((ip, port))
@ -437,7 +448,7 @@ class TcpSrv(object):
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
from .stolen.ifaddr import get_adapters from .stolen.ifaddr import get_adapters
listen_ips = [x for x in listen_ips if "unix:" not in x] listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
nics = get_adapters(True) nics = get_adapters(True)
eps: dict[str, Netdev] = {} eps: dict[str, Netdev] = {}
@ -583,8 +594,7 @@ class TcpSrv(object):
if not ip: if not ip:
return "" return ""
if ":" in ip: hip = "[%s]" % (ip,) if ":" in ip else ip
ip = "[{}]".format(ip)
if self.args.http_only: if self.args.http_only:
https = "" https = ""
@ -596,7 +606,7 @@ class TcpSrv(object):
ports = t1.get(ip, t2.get(ip, [])) ports = t1.get(ip, t2.get(ip, []))
dport = 443 if https else 80 dport = 443 if https else 80
port = "" if dport in ports or not ports else ":{}".format(ports[0]) port = "" if dport in ports or not ports else ":{}".format(ports[0])
txt = "http{}://{}{}/{}".format(https, ip, port, self.args.qrl) txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl)
btxt = txt.encode("utf-8") btxt = txt.encode("utf-8")
if PY2: if PY2:
@ -604,6 +614,10 @@ class TcpSrv(object):
fg = self.args.qr_fg fg = self.args.qr_fg
bg = self.args.qr_bg bg = self.args.qr_bg
nocolor = fg == -1
if nocolor:
fg = 0
pad = self.args.qrp pad = self.args.qrp
zoom = self.args.qrz zoom = self.args.qrz
qrc = QrCode.encode_binary(btxt) qrc = QrCode.encode_binary(btxt)
@ -631,6 +645,8 @@ class TcpSrv(object):
qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do
cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m" cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m"
if nocolor:
cc = " \033[0m"
t = cc + "\n{2}\033[999G\033[0m\033[J" t = cc + "\n{2}\033[999G\033[0m\033[J"
t = t.format(fg, bg, qr) t = t.format(fg, bg, qr)
if ANYWIN: if ANYWIN:

View file

@ -45,6 +45,7 @@ from .util import (
exclude_dotfiles, exclude_dotfiles,
min_ex, min_ex,
runhook, runhook,
set_fperms,
undot, undot,
vjoin, vjoin,
vsplit, vsplit,
@ -178,7 +179,7 @@ class Tftpd(object):
if "::" in ips: if "::" in ips:
ips.append("0.0.0.0") ips.append("0.0.0.0")
ips = [x for x in ips if "unix:" not in x] ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
if self.args.tftp4: if self.args.tftp4:
ips = [x for x in ips if ":" not in x] ips = [x for x in ips if ":" not in x]
@ -388,8 +389,8 @@ class Tftpd(object):
a = (self.args.iobuf,) a = (self.args.iobuf,)
ret = open(ap, mode, *a, **ka) ret = open(ap, mode, *a, **ka)
if wr and "chmod_f" in vfs.flags: if wr and "fperms" in vfs.flags:
os.fchmod(ret.fileno(), vfs.flags["chmod_f"]) set_fperms(ret, vfs.flags)
return ret return ret
@ -398,7 +399,9 @@ class Tftpd(object):
if "*" not in vfs.axs.uwrite: if "*" not in vfs.axs.uwrite:
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,)) yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
return bos.mkdir(ap, vfs.flags["chmod_d"]) bos.mkdir(ap, vfs.flags["chmod_d"])
if "chown" in vfs.flags:
bos.chown(ap, vfs.flags["uid"], vfs.flags["gid"])
def _unlink(self, vpath: str) -> None: def _unlink(self, vpath: str) -> None:
# return bos.unlink(self._v2a("stat", vpath, *a)[1]) # return bos.unlink(self._v2a("stat", vpath, *a)[1])

View file

@ -36,11 +36,15 @@ class ThumbCli(object):
if not c: if not c:
raise Exception() raise Exception()
except: except:
c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]} c = {
k: set()
for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"]
}
self.thumbable = c["thumbable"] self.thumbable = c["thumbable"]
self.fmt_pil = c["pil"] self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"] self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
self.fmt_ffi = c["ffi"] self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"] self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"] self.fmt_ffa = c["ffa"]
@ -88,7 +92,7 @@ class ThumbCli(object):
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]: if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
return os.path.join(ptop, rem) return os.path.join(ptop, rem)
if fmt[:1] in "jw": if fmt[:1] in "jw" and fmt != "wav":
sfmt = fmt[:1] sfmt = fmt[:1]
if sfmt == "j" and self.args.th_no_jpg: if sfmt == "j" and self.args.th_no_jpg:
@ -129,7 +133,7 @@ class ThumbCli(object):
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa) tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
tpaths = [tpath] tpaths = [tpath]
if fmt[:1] == "w": if fmt[:1] == "w" and fmt != "wav":
# also check for jpg (maybe webp is unavailable) # also check for jpg (maybe webp is unavailable)
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg") tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import hashlib import hashlib
import io
import logging import logging
import os import os
import re import re
@ -50,7 +51,7 @@ HAVE_AVIF = False
HAVE_WEBP = False HAVE_WEBP = False
EXTS_TH = set(["jpg", "webp", "png"]) EXTS_TH = set(["jpg", "webp", "png"])
EXTS_AC = set(["opus", "owa", "caf", "mp3"]) EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"])
EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split()) EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split())
PTN_TS = re.compile("^-?[0-9a-f]{8,10}$") PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
@ -85,7 +86,10 @@ try:
if os.environ.get("PRTY_NO_PIL_HEIF"): if os.environ.get("PRTY_NO_PIL_HEIF"):
raise Exception() raise Exception()
from pyheif_pillow_opener import register_heif_opener try:
from pillow_heif import register_heif_opener
except ImportError:
from pyheif_pillow_opener import register_heif_opener
register_heif_opener() register_heif_opener()
HAVE_HEIF = True HAVE_HEIF = True
@ -112,14 +116,28 @@ except:
try: try:
if os.environ.get("PRTY_NO_VIPS"): if os.environ.get("PRTY_NO_VIPS"):
raise Exception() raise ImportError()
HAVE_VIPS = True HAVE_VIPS = True
import pyvips import pyvips
logging.getLogger("pyvips").setLevel(logging.WARNING) logging.getLogger("pyvips").setLevel(logging.WARNING)
except: except Exception as e:
HAVE_VIPS = False HAVE_VIPS = False
if not isinstance(e, ImportError):
logging.warning("libvips found, but failed to load: " + str(e))
try:
if os.environ.get("PRTY_NO_RAW"):
raise Exception()
HAVE_RAW = True
import rawpy
logging.getLogger("rawpy").setLevel(logging.WARNING)
except:
HAVE_RAW = False
th_dir_cache = {} th_dir_cache = {}
@ -205,11 +223,19 @@ class ThumbSrv(object):
if self.args.th_clean: if self.args.th_clean:
Daemon(self.cleaner, "thumb.cln") Daemon(self.cleaner, "thumb.cln")
self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [ (
self.fmt_pil,
self.fmt_vips,
self.fmt_raw,
self.fmt_ffi,
self.fmt_ffv,
self.fmt_ffa,
) = [
set(y.split(",")) set(y.split(","))
for y in [ for y in [
self.args.th_r_pil, self.args.th_r_pil,
self.args.th_r_vips, self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi, self.args.th_r_ffi,
self.args.th_r_ffv, self.args.th_r_ffv,
self.args.th_r_ffa, self.args.th_r_ffa,
@ -232,6 +258,9 @@ class ThumbSrv(object):
if "vips" in self.args.th_dec: if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips self.thumbable |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
if "ff" in self.args.th_dec: if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss self.thumbable |= zss
@ -269,8 +298,8 @@ class ThumbSrv(object):
self.log("joined waiting room for %r" % (tpath,)) self.log("joined waiting room for %r" % (tpath,))
except: except:
thdir = os.path.dirname(tpath) thdir = os.path.dirname(tpath)
chmod = 0o700 if self.args.free_umask else 0o755 chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755
bos.makedirs(os.path.join(thdir, "w"), chmod) bos.makedirs(os.path.join(thdir, "w"), vf=chmod)
inf_path = os.path.join(thdir, "dir.txt") inf_path = os.path.join(thdir, "dir.txt")
if not bos.path.exists(inf_path): if not bos.path.exists(inf_path):
@ -313,6 +342,7 @@ class ThumbSrv(object):
"thumbable": self.thumbable, "thumbable": self.thumbable,
"pil": self.fmt_pil, "pil": self.fmt_pil,
"vips": self.fmt_vips, "vips": self.fmt_vips,
"raw": self.fmt_raw,
"ffi": self.fmt_ffi, "ffi": self.fmt_ffi,
"ffv": self.fmt_ffv, "ffv": self.fmt_ffv,
"ffa": self.fmt_ffa, "ffa": self.fmt_ffa,
@ -355,8 +385,10 @@ class ThumbSrv(object):
tex = tpath.rsplit(".", 1)[-1] tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3" want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf") want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png" want_png = tex == "png"
want_au = want_mp3 or want_opus want_au = want_mp3 or want_opus or want_flac or want_wav
for lib in self.args.th_dec: for lib in self.args.th_dec:
can_au = lib == "ff" and ( can_au = lib == "ff" and (
ext in self.fmt_ffa or ext in self.fmt_ffv ext in self.fmt_ffa or ext in self.fmt_ffv
@ -366,11 +398,17 @@ class ThumbSrv(object):
funs.append(self.conv_pil) funs.append(self.conv_pil)
elif lib == "vips" and ext in self.fmt_vips: elif lib == "vips" and ext in self.fmt_vips:
funs.append(self.conv_vips) funs.append(self.conv_vips)
elif lib == "raw" and ext in self.fmt_raw:
funs.append(self.conv_raw)
elif can_au and (want_png or want_au): elif can_au and (want_png or want_au):
if want_opus: if want_opus:
funs.append(self.conv_opus) funs.append(self.conv_opus)
elif want_mp3: elif want_mp3:
funs.append(self.conv_mp3) funs.append(self.conv_mp3)
elif want_flac:
funs.append(self.conv_flac)
elif want_wav:
funs.append(self.conv_wav)
elif want_png: elif want_png:
funs.append(self.conv_waves) funs.append(self.conv_waves)
png_ok = True png_ok = True
@ -474,35 +512,38 @@ class ThumbSrv(object):
return im return im
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
try:
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.getres(vn, fmt))
fmts = ["RGB", "L"]
args = {"quality": 40}
if tpath.endswith(".webp"):
# quality 80 = pillow-default
# quality 75 = ffmpeg-default
# method 0 = pillow-default, fast
# method 4 = ffmpeg-default
# method 6 = max, slow
fmts.extend(("RGBA", "LA"))
args["method"] = 6
else:
# default q = 75
args["progressive"] = True
if im.mode not in fmts:
# print("conv {}".format(im.mode))
im = im.convert("RGB")
im.save(tpath, **args)
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im: with Image.open(fsenc(abspath)) as im:
try: self.conv_image_pil(im, tpath, fmt, vn)
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.getres(vn, fmt))
fmts = ["RGB", "L"]
args = {"quality": 40}
if tpath.endswith(".webp"):
# quality 80 = pillow-default
# quality 75 = ffmpeg-default
# method 0 = pillow-default, fast
# method 4 = ffmpeg-default
# method 6 = max, slow
fmts.extend(("RGBA", "LA"))
args["method"] = 6
else:
# default q = 75
args["progressive"] = True
if im.mode not in fmts:
# print("conv {}".format(im.mode))
im = im.convert("RGB")
im.save(tpath, **args)
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
@ -525,9 +566,53 @@ class ThumbSrv(object):
assert img # type: ignore # !rm assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40) img.write_to_file(tpath, Q=40)
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with rawpy.imread(abspath) as raw:
thumb = raw.extract_thumb()
if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"):
# if we have a jpg thumbnail and no webp output is available,
# just write the jpg directly (it'll be the wrong size, but it's fast)
with open(tpath, "wb") as f:
f.write(thumb.data)
if HAVE_VIPS:
crops = ["centre", "none"]
if "f" in fmt:
crops = ["none"]
w, h = self.getres(vn, fmt)
kw = {"height": h, "size": "down", "intent": "relative"}
for c in crops:
try:
kw["crop"] = c
if thumb.format == rawpy.ThumbFormat.BITMAP:
img = pyvips.Image.new_from_array(
thumb.data, interpretation="rgb"
)
img = img.thumbnail_image(w, **kw)
else:
img = pyvips.Image.thumbnail_buffer(thumb.data, w, **kw)
break
except:
if c == crops[-1]:
raise
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
elif HAVE_PIL:
if thumb.format == rawpy.ThumbFormat.BITMAP:
im = Image.fromarray(thumb.data, "RGB")
else:
im = Image.open(io.BytesIO(thumb.data))
self.conv_image_pil(im, tpath, fmt, vn)
else:
raise Exception(
"either pil or vips is needed to process embedded bitmap thumbnails in raw files"
)
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret: if not ret:
return return
@ -538,6 +623,17 @@ class ThumbSrv(object):
dur = ret[".dur"][1] if ".dur" in ret else 4 dur = ret[".dur"][1] if ".dur" in ret else 4
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0")
def _ffmpeg_im(
self,
abspath: str,
tpath: str,
fmt: str,
vn: VFS,
seek: list[bytes],
imap: bytes,
) -> None:
scale = "scale={0}:{1}:force_original_aspect_ratio=" scale = "scale={0}:{1}:force_original_aspect_ratio="
if "f" in fmt: if "f" in fmt:
scale += "decrease,setsar=1:1" scale += "decrease,setsar=1:1"
@ -556,7 +652,7 @@ class ThumbSrv(object):
cmd += seek cmd += seek
cmd += [ cmd += [
b"-i", fsenc(abspath), b"-i", fsenc(abspath),
b"-map", b"0:v:0", b"-map", imap,
b"-vf", bscale, b"-vf", bscale,
b"-frames:v", b"1", b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0", b"-metadata:s:v:0", b"rotate=0",
@ -577,11 +673,11 @@ class ThumbSrv(object):
] ]
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd, vn) self._run_ff(cmd, vn, "convt")
def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None: def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None:
# self.log((b" ".join(cmd)).decode("utf-8")) # self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom) ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom)
if not ret: if not ret:
return return
@ -625,7 +721,7 @@ class ThumbSrv(object):
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
@ -663,7 +759,7 @@ class ThumbSrv(object):
# fmt: on # fmt: on
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd, vn) self._run_ff(cmd, vn, "convt")
if "pngquant" in vn.flags: if "pngquant" in vn.flags:
wtpath = tpath + ".png" wtpath = tpath + ".png"
@ -684,11 +780,31 @@ class ThumbSrv(object):
else: else:
atomic_move(self.log, wtpath, tpath, vn.flags) atomic_move(self.log, wtpath, tpath, vn.flags)
def conv_emb_cv(
self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any]
) -> None:
self.wait4ram(0.2, tpath)
self._ffmpeg_im(
abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii")
)
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
want_spec = vn.flags.get("th_spec_p", 1)
if want_spec < 2:
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)
if not want_spec:
raise Exception("spectrograms forbidden by volflag")
fext = abspath.split(".")[-1].lower() fext = abspath.split(".")[-1].lower()
# https://trac.ffmpeg.org/ticket/10797 # https://trac.ffmpeg.org/ticket/10797
@ -724,7 +840,7 @@ class ThumbSrv(object):
b"-y", fsenc(infile), b"-y", fsenc(infile),
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn) self._run_ff(cmd, vn, "convt")
fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
if "3" in fmt: if "3" in fmt:
@ -766,7 +882,7 @@ class ThumbSrv(object):
] ]
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd, vn) self._run_ff(cmd, vn, "convt")
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
quality = self.args.q_mp3.lower() quality = self.args.q_mp3.lower()
@ -774,7 +890,7 @@ class ThumbSrv(object):
raise Exception("disabled in server config") raise Exception("disabled in server config")
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags: if "ac" not in tags:
raise Exception("not audio") raise Exception("not audio")
@ -805,14 +921,74 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.allow_flac:
raise Exception("flac not permitted in server config")
self.wait4ram(0.2, tpath)
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
self.log("conv2 flac", 6)
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-c:a", b"flac",
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.allow_wav:
raise Exception("wav not permitted in server config")
self.wait4ram(0.2, tpath)
tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags:
raise Exception("not audio")
bits = tags[".bps"][1]
if bits == 0.0:
bits = tags[".bprs"][1]
codec = b"pcm_s32le"
if bits <= 16.0:
codec = b"pcm_s16le"
elif bits <= 24.0:
codec = b"pcm_s24le"
self.log("conv2 wav", 6)
# fmt: off
cmd = [
b"ffmpeg",
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-map", b"0:a:0",
b"-c:a", codec,
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, "aconvt", oom=300)
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode or not self.args.q_opus: if self.args.no_acode or not self.args.q_opus:
raise Exception("disabled in server config") raise Exception("disabled in server config")
self.wait4ram(0.2, tpath) self.wait4ram(0.2, tpath)
tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in tags: if "ac" not in tags:
raise Exception("not audio") raise Exception("not audio")
@ -861,7 +1037,7 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
def _conv_caf( def _conv_caf(
self, self,
@ -901,7 +1077,7 @@ class ThumbSrv(object):
fsenc(tmp_opus) fsenc(tmp_opus)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
# iOS fails to play some "insufficiently complex" files # iOS fails to play some "insufficiently complex" files
# (average file shorter than 8 seconds), so of course we # (average file shorter than 8 seconds), so of course we
@ -928,7 +1104,7 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
else: else:
# simple remux should be safe # simple remux should be safe
@ -947,7 +1123,7 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd, vn, oom=300) self._run_ff(cmd, vn, "aconvt", oom=300)
try: try:
wunlink(self.log, tmp_opus, vn.flags) wunlink(self.log, tmp_opus, vn.flags)

View file

@ -391,7 +391,7 @@ class U2idx(object):
fk_alg = 2 if "fka" in flags else 1 fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv)) c = cur.execute(uq, tuple(vuv))
for hit in c: for hit in c:
w, ts, sz, rd, fn, ip, at = hit[:7] w, ts, sz, rd, fn = hit[:5]
if rd.startswith("//") or fn.startswith("//"): if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn) rd, fn = s3dec(rd, fn)

View file

@ -77,7 +77,7 @@ except:
if HAVE_SQLITE3: if HAVE_SQLITE3:
import sqlite3 import sqlite3
DB_VER = 5 DB_VER = 6
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Pattern, Union from typing import Any, Optional, Pattern, Union
@ -86,7 +86,10 @@ if TYPE_CHECKING:
from .svchub import SvcHub from .svchub import SvcHub
zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp" zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
CV_EXTS = set(zsg.split(",")) ICV_EXTS = set(zsg.split(","))
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
VCV_EXTS = set(zsg.split(","))
zsg = "nohash noidx xdev xvol" zsg = "nohash noidx xdev xvol"
VF_AFFECTS_INDEXING = set(zsg.split(" ")) VF_AFFECTS_INDEXING = set(zsg.split(" "))
@ -141,6 +144,7 @@ class Up2k(object):
self.salt = self.args.warksalt self.salt = self.args.warksalt
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
self.abrt_key = ""
self.gid = 0 self.gid = 0
self.gt0 = 0 self.gt0 = 0
@ -372,11 +376,12 @@ class Up2k(object):
if ineed == ihash or not ineed: if ineed == ihash or not ineed:
continue continue
poke = job["poke"]
zt = ( zt = (
ineed / ihash, ineed / ihash,
job["size"], job["size"],
int(job["t0c"]), int(job.get("t0c", poke)),
int(job["poke"]), int(poke),
djoin(vtop, job["prel"], job["name"]), djoin(vtop, job["prel"], job["name"]),
) )
ret.append(zt) ret.append(zt)
@ -399,12 +404,14 @@ class Up2k(object):
return "{}" return "{}"
def get_unfinished_by_user(self, uname, ip) -> str: def get_unfinished_by_user(self, uname, ip) -> dict[str, Any]:
# returns dict due to ExceptionalQueue
if PY2 or not self.reg_mutex.acquire(timeout=2): if PY2 or not self.reg_mutex.acquire(timeout=2):
return '[{"timeout":1}]' return {"timeout": 1}
ret: list[tuple[int, str, int, int, int]] = [] ret: list[tuple[int, str, int, int, int]] = []
userset = set([(uname or "\n"), "*"]) userset = set([(uname or "\n"), "*"])
n = 1000
try: try:
for ptop, tab2 in self.registry.items(): for ptop, tab2 in self.registry.items():
cfg = self.flags.get(ptop, {}).get("u2abort", 1) cfg = self.flags.get(ptop, {}).get("u2abort", 1)
@ -419,7 +426,6 @@ class Up2k(object):
or (addr and addr != job["addr"]) or (addr and addr != job["addr"])
): ):
continue continue
zt5 = ( zt5 = (
int(job["t0"]), int(job["t0"]),
djoin(job["vtop"], job["prel"], job["name"]), djoin(job["vtop"], job["prel"], job["name"]),
@ -428,6 +434,9 @@ class Up2k(object):
len(job["hash"]), len(job["hash"]),
) )
ret.append(zt5) ret.append(zt5)
n -= 1
if not n:
break
finally: finally:
self.reg_mutex.release() self.reg_mutex.release()
@ -444,7 +453,7 @@ class Up2k(object):
} }
for (at, vp, sz, nn, nh) in ret for (at, vp, sz, nn, nh) in ret
] ]
return json.dumps(ret2, separators=(",\n", ": ")) return {"f": ret2}
def get_unfinished(self) -> str: def get_unfinished(self) -> str:
if PY2 or not self.reg_mutex.acquire(timeout=0.5): if PY2 or not self.reg_mutex.acquire(timeout=0.5):
@ -894,7 +903,7 @@ class Up2k(object):
self.iacct = self.asrv.iacct self.iacct = self.asrv.iacct
self.grps = self.asrv.grps self.grps = self.asrv.grps
have_e2d = self.args.idp_h_usr or self.args.chpw or self.args.shr have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr
vols = list(all_vols.values()) vols = list(all_vols.values())
t0 = time.time() t0 = time.time()
@ -916,7 +925,7 @@ class Up2k(object):
for vol in vols: for vol in vols:
try: try:
# mkdir gonna happen at snap anyways; # mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vol.flags["chmod_d"]) bos.makedirs(vol.realpath, vf=vol.flags)
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath) dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
except Exception as ex: except Exception as ex:
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
@ -1474,7 +1483,7 @@ class Up2k(object):
unreg: list[str] = [] unreg: list[str] = []
files: list[tuple[int, int, str]] = [] files: list[tuple[int, int, str]] = []
fat32 = True fat32 = True
cv = "" cv = vcv = ""
th_cvd = self.args.th_coversd th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set th_cvds = self.args.th_coversd_set
@ -1569,25 +1578,24 @@ class Up2k(object):
rsz += sz rsz += sz
files.append((sz, lmod, iname)) files.append((sz, lmod, iname))
liname = iname.lower() if sz:
if ( liname = iname.lower()
sz ext = liname.rsplit(".", 1)[-1]
and ( if (
liname in th_cvds liname in th_cvds
or ( or (not cv and ext in ICV_EXTS and not iname.startswith("."))
not cv ) and (
and liname.rsplit(".", 1)[-1] in CV_EXTS
and not iname.startswith(".")
)
)
and (
not cv not cv
or liname not in th_cvds or liname not in th_cvds
or cv.lower() not in th_cvds or cv.lower() not in th_cvds
or th_cvd.index(liname) < th_cvd.index(cv.lower()) or th_cvd.index(liname) < th_cvd.index(cv.lower())
) ):
): cv = iname
cv = iname elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
vcv = iname
if not cv:
cv = vcv
if not self.args.no_dirsz: if not self.args.no_dirsz:
tnf += len(files) tnf += len(files)
@ -1647,7 +1655,7 @@ class Up2k(object):
abspath = cdirs + fn abspath = cdirs + fn
nohash = reh.search(abspath) if reh else False nohash = reh.search(abspath) if reh else False
sql = "select w, mt, sz, ip, at from up where rd = ? and fn = ?" sql = "select w, mt, sz, ip, at, un from up where rd = ? and fn = ?"
try: try:
c = db.c.execute(sql, (rd, fn)) c = db.c.execute(sql, (rd, fn))
except: except:
@ -1656,7 +1664,7 @@ class Up2k(object):
in_db = list(c.fetchall()) in_db = list(c.fetchall())
if in_db: if in_db:
self.pp.n -= 1 self.pp.n -= 1
dw, dts, dsz, ip, at = in_db[0] dw, dts, dsz, ip, at, un = in_db[0]
if len(in_db) > 1: if len(in_db) > 1:
t = "WARN: multiple entries: %r => %r |%d|\n%r" t = "WARN: multiple entries: %r => %r |%d|\n%r"
rep_db = "\n".join([repr(x) for x in in_db]) rep_db = "\n".join([repr(x) for x in in_db])
@ -1669,6 +1677,9 @@ class Up2k(object):
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz): if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
continue continue
if un is None:
un = ""
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
self.log(t % (top, rp, dts, lmod, dsz, sz)) self.log(t % (top, rp, dts, lmod, dsz, sz))
self.db_rm(db.c, rd, fn, 0) self.db_rm(db.c, rd, fn, 0)
@ -1679,6 +1690,7 @@ class Up2k(object):
dw = "" dw = ""
ip = "" ip = ""
at = 0 at = 0
un = ""
self.pp.msg = "a%d %s" % (self.pp.n, abspath) self.pp.msg = "a%d %s" % (self.pp.n, abspath)
@ -1704,9 +1716,10 @@ class Up2k(object):
if dw and dw != wark: if dw and dw != wark:
ip = "" ip = ""
at = 0 at = 0
un = ""
# skip upload hooks by not providing vflags # skip upload hooks by not providing vflags
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", "", ip, at) self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
db.n += 1 db.n += 1
db.nf += 1 db.nf += 1
tfa += 1 tfa += 1
@ -2143,8 +2156,8 @@ class Up2k(object):
with self.mutex: with self.mutex:
try: try:
q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?" q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone() rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone()
except: except:
# file modified/deleted since spooling # file modified/deleted since spooling
continue continue
@ -2163,12 +2176,15 @@ class Up2k(object):
abspath = djoin(ptop, rd, fn) abspath = djoin(ptop, rd, fn)
self.pp.msg = "c%d %s" % (nq, abspath) self.pp.msg = "c%d %s" % (nq, abspath)
if not mpool: if not mpool:
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at) n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un)
else: else:
oth_tags = {}
if ip: if ip:
oth_tags = {"up_ip": ip, "up_at": at} oth_tags["up_ip"] = ip
else: if at:
oth_tags = {} oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
mpool.put(Mpqe({}, entags, w, abspath, oth_tags)) mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
with self.mutex: with self.mutex:
@ -2324,8 +2340,8 @@ class Up2k(object):
if w in in_progress: if w in in_progress:
continue continue
q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1" q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1"
rd, fn, ip, at = cur.execute(q, (w,)).fetchone() rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone()
rd, fn = s3dec(rd, fn) rd, fn = s3dec(rd, fn)
abspath = djoin(ptop, rd, fn) abspath = djoin(ptop, rd, fn)
@ -2349,7 +2365,10 @@ class Up2k(object):
if ip: if ip:
oth_tags["up_ip"] = ip oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags)) jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags))
in_progress[w] = True in_progress[w] = True
@ -2538,6 +2557,7 @@ class Up2k(object):
abspath: str, abspath: str,
ip: str, ip: str,
at: float, at: float,
un: Optional[str],
) -> int: ) -> int:
"""will mutex(main)""" """will mutex(main)"""
assert self.mtag # !rm assert self.mtag # !rm
@ -2558,7 +2578,10 @@ class Up2k(object):
if ip: if ip:
tags["up_ip"] = ip tags["up_ip"] = ip
if at:
tags["up_at"] = at tags["up_at"] = at
if un:
tags["up_by"] = un
with self.mutex: with self.mutex:
return self._tag_file(write_cur, entags, wark, abspath, tags) return self._tag_file(write_cur, entags, wark, abspath, tags)
@ -2662,16 +2685,19 @@ class Up2k(object):
if not existed and ver is None: if not existed and ver is None:
return self._try_create_db(db_path, cur) return self._try_create_db(db_path, cur)
if ver == 4: for upver in (4, 5):
if ver != upver:
continue
try: try:
t = "creating backup before upgrade: " t = "creating backup before upgrade: "
cur = self._backup_db(db_path, cur, ver, t) cur = self._backup_db(db_path, cur, ver, t)
self._upgrade_v4(cur) getattr(self, "_upgrade_v%d" % (upver,))(cur)
ver = 5 ver += 1 # type: ignore
except: except:
self.log("WARN: failed to upgrade from v4", 3) self.log("WARN: failed to upgrade from v%d" % (ver,), 3)
if ver == DB_VER: if ver == DB_VER:
# these no longer serve their intended purpose but they're great as additional sanchks
self._add_dhash_tab(cur) self._add_dhash_tab(cur)
self._add_xiu_tab(cur) self._add_xiu_tab(cur)
self._add_cv_tab(cur) self._add_cv_tab(cur)
@ -2773,7 +2799,7 @@ class Up2k(object):
idx = r"create index up_w on up(w)" idx = r"create index up_w on up(w)"
for cmd in [ for cmd in [
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)", r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)",
r"create index up_vp on up(rd, fn)", r"create index up_vp on up(rd, fn)",
r"create index up_fn on up(fn)", r"create index up_fn on up(fn)",
r"create index up_ip on up(ip)", r"create index up_ip on up(ip)",
@ -2806,6 +2832,15 @@ class Up2k(object):
cur.connection.commit() cur.connection.commit()
def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None:
for cmd in [
r"alter table up add column un text",
r"update kv set v=6 where k='sver'",
]:
cur.execute(cmd)
cur.connection.commit()
def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None: def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None:
# v5 -> v5a # v5 -> v5a
try: try:
@ -2827,7 +2862,7 @@ class Up2k(object):
# v5a -> v5b # v5a -> v5b
# store rd+fn rather than warks to support nohash vols # store rd+fn rather than warks to support nohash vols
try: try:
cur.execute("select ws, rd, fn from iu limit 1").fetchone() cur.execute("select c, w, rd, fn from iu limit 1").fetchone()
return return
except: except:
pass pass
@ -3003,7 +3038,7 @@ class Up2k(object):
argv = [dwark[:16], dwark] argv = [dwark[:16], dwark]
c2 = cur.execute(q, tuple(argv)) c2 = cur.execute(q, tuple(argv))
for _, dtime, dsize, dp_dir, dp_fn, ip, at in c2: for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2:
if dp_dir.startswith("//") or dp_fn.startswith("//"): if dp_dir.startswith("//") or dp_fn.startswith("//"):
dp_dir, dp_fn = s3dec(dp_dir, dp_fn) dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
@ -3309,7 +3344,7 @@ class Up2k(object):
reg, reg,
"up2k._get_volsize", "up2k._get_volsize",
) )
bos.makedirs(ap2, vfs.flags["chmod_d"]) bos.makedirs(ap2, vf=vfs.flags)
vfs.lim.nup(cj["addr"]) vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"]) vfs.lim.bup(cj["addr"], cj["size"])
@ -3425,7 +3460,7 @@ class Up2k(object):
try: try:
vrel = vjoin(job["prel"], fname) vrel = vjoin(job["prel"], fname)
xlink = bool(vf.get("xlink")) xlink = bool(vf.get("xlink"))
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, vrel) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink) self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
except Exception as ex: except Exception as ex:
self.log("skipping replace-relink: %r" % (ex,)) self.log("skipping replace-relink: %r" % (ex,))
@ -3445,7 +3480,7 @@ class Up2k(object):
"wb", "wb",
fdir=fdir, fdir=fdir,
suffix="-%.6f-%s" % (ts, dip), suffix="-%.6f-%s" % (ts, dip),
chmod=vf.get("chmod_f", -1), vf=vf,
) )
f.close() f.close()
return ret return ret
@ -3476,6 +3511,8 @@ class Up2k(object):
linked = False linked = False
try: try:
if "reflink" in flags:
raise Exception("reflink")
if not is_mv and not flags.get("dedup"): if not is_mv and not flags.get("dedup"):
raise Exception("dedup is disabled in config") raise Exception("dedup is disabled in config")
@ -3532,7 +3569,8 @@ class Up2k(object):
linked = True linked = True
except Exception as ex: except Exception as ex:
self.log("cannot link; creating copy: " + repr(ex)) if str(ex) != "reflink":
self.log("cannot link; creating copy: " + repr(ex))
if bos.path.isfile(src): if bos.path.isfile(src):
csrc = src csrc = src
elif fsrc and bos.path.isfile(fsrc): elif fsrc and bos.path.isfile(fsrc):
@ -3879,14 +3917,14 @@ class Up2k(object):
# plugins may expect this to look like an actual IP # plugins may expect this to look like an actual IP
db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip
sql = "insert into up values (?,?,?,?,?,?,?)" sql = "insert into up values (?,?,?,?,?,?,?,?)"
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0)) v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
try: try:
db.execute(sql, v) db.execute(sql, v)
except: except:
assert self.mem_cur # !rm assert self.mem_cur # !rm
rd, fn = s3enc(self.mem_cur, rd, fn) rd, fn = s3enc(self.mem_cur, rd, fn)
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0)) v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
db.execute(sql, v) db.execute(sql, v)
self.volsize[db] += sz self.volsize[db] += sz
@ -3978,6 +4016,9 @@ class Up2k(object):
except: except:
pass pass
def handle_fs_abrt(self, akey: str) -> None:
self.abrt_key = akey
def handle_rm( def handle_rm(
self, self,
uname: str, uname: str,
@ -4024,7 +4065,7 @@ class Up2k(object):
vn, rem = vn0.get_dbv(rem0) vn, rem = vn0.get_dbv(rem0)
ptop = vn.realpath ptop = vn.realpath
with self.mutex, self.reg_mutex: with self.mutex, self.reg_mutex:
abrt_cfg = self.flags.get(ptop, {}).get("u2abort", 1) abrt_cfg = vn.flags.get("u2abort", 1)
addr = (ip or "\n") if abrt_cfg in (1, 2) else "" addr = (ip or "\n") if abrt_cfg in (1, 2) else ""
user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None
reg = self.registry.get(ptop, {}) if abrt_cfg else {} reg = self.registry.get(ptop, {}) if abrt_cfg else {}
@ -4045,17 +4086,22 @@ class Up2k(object):
if partial: if partial:
dip = ip dip = ip
dat = time.time() dat = time.time()
dun = uname
un_cfg = 1
else: else:
if not self.args.unpost: un_cfg = vn.flags["unp_who"]
if not self.args.unpost or not un_cfg:
t = "the unpost feature is disabled in server config" t = "the unpost feature is disabled in server config"
raise Pebkac(400, t) raise Pebkac(400, t)
_, _, _, _, dip, dat = self._find_from_vpath(ptop, rem) _, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem)
t = "you cannot delete this: " t = "you cannot delete this: "
if not dip: if not dip:
t += "file not found" t += "file not found"
elif dip != ip: elif dip != ip and un_cfg in (1, 2):
t += "not uploaded by (You)"
elif dun != uname and un_cfg in (1, 3):
t += "not uploaded by (You)" t += "not uploaded by (You)"
elif dat < time.time() - self.args.unpost: elif dat < time.time() - self.args.unpost:
t += "uploaded too long ago" t += "uploaded too long ago"
@ -4144,7 +4190,7 @@ class Up2k(object):
try: try:
ptop = dbv.realpath ptop = dbv.realpath
xlink = bool(dbv.flags.get("xlink")) xlink = bool(dbv.flags.get("xlink"))
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file( self._forget_file(
ptop, volpath, cur, wark, True, st.st_size, xlink ptop, volpath, cur, wark, True, st.st_size, xlink
) )
@ -4187,7 +4233,7 @@ class Up2k(object):
return n_files, ok + ok2, ng + ng2 return n_files, ok + ok2, ng + ng2
def handle_cp(self, uname: str, ip: str, svp: str, dvp: str) -> str: def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"): if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "cp: cannot copy parent into subfolder") raise Pebkac(400, "cp: cannot copy parent into subfolder")
@ -4234,6 +4280,8 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :] dvpf = dvp + svpf[len(svp) :]
self._cp_file(uname, ip, svpf, dvpf, curs) self._cp_file(uname, ip, svpf, dvpf, curs)
if abrt and abrt == self.abrt_key:
raise Pebkac(400, "filecopy aborted by http-api")
for v in curs: for v in curs:
v.connection.commit() v.connection.commit()
@ -4301,9 +4349,9 @@ class Up2k(object):
self.log(t, 1) self.log(t, 1)
raise Pebkac(405, t) raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"]) bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath( c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(
svn_dbv.realpath, srem_dbv svn_dbv.realpath, srem_dbv
) )
c2 = self.cur.get(dvn.realpath) c2 = self.cur.get(dvn.realpath)
@ -4328,7 +4376,7 @@ class Up2k(object):
w, w,
w, w,
"", "",
"", un or "",
ip or "", ip or "",
at or 0, at or 0,
) )
@ -4401,7 +4449,7 @@ class Up2k(object):
return "k" return "k"
def handle_mv(self, uname: str, ip: str, svp: str, dvp: str) -> str: def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"): if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "mv: cannot move parent into subfolder") raise Pebkac(400, "mv: cannot move parent into subfolder")
@ -4456,6 +4504,8 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :] dvpf = dvp + svpf[len(svp) :]
self._mv_file(uname, ip, svpf, dvpf, curs) self._mv_file(uname, ip, svpf, dvpf, curs)
if abrt and abrt == self.abrt_key:
raise Pebkac(400, "filemove aborted by http-api")
for v in curs: for v in curs:
v.connection.commit() v.connection.commit()
@ -4477,7 +4527,10 @@ class Up2k(object):
vp = vjoin(dvp, rem) vp = vjoin(dvp, rem)
try: try:
dvn, drem = self.vfs.get(vp, uname, False, True) dvn, drem = self.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem), dvn.flags["chmod_d"]) dap = dvn.canonical(drem)
bos.mkdir(dap, dvn.flags["chmod_d"])
if "chown" in dvn.flags:
bos.chown(dap, dvn.flags["uid"], dvn.flags["gid"])
except: except:
pass pass
@ -4547,7 +4600,7 @@ class Up2k(object):
is_xvol = svn.realpath != dvn.realpath is_xvol = svn.realpath != dvn.realpath
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"]) bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
if is_dirlink: if is_dirlink:
dlabs = absreal(sabs) dlabs = absreal(sabs)
@ -4584,7 +4637,7 @@ class Up2k(object):
return "k" return "k"
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(svn.realpath, srem) c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem)
c2 = self.cur.get(dvn.realpath) c2 = self.cur.get(dvn.realpath)
has_dupes = False has_dupes = False
@ -4618,7 +4671,7 @@ class Up2k(object):
w, w,
w, w,
"", "",
"", un or "",
ip or "", ip or "",
at or 0, at or 0,
) )
@ -4718,13 +4771,14 @@ class Up2k(object):
Optional[int], Optional[int],
str, str,
Optional[int], Optional[int],
str,
]: ]:
cur = self.cur.get(ptop) cur = self.cur.get(ptop)
if not cur: if not cur:
return None, None, None, None, "", None return None, None, None, None, "", None, ""
rd, fn = vsplit(vrem) rd, fn = vsplit(vrem)
q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1" q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1"
try: try:
c = cur.execute(q, (rd, fn)) c = cur.execute(q, (rd, fn))
except: except:
@ -4733,9 +4787,9 @@ class Up2k(object):
hit = c.fetchone() hit = c.fetchone()
if hit: if hit:
wark, ftime, fsize, ip, at = hit wark, ftime, fsize, ip, at, un = hit
return cur, wark, ftime, fsize, ip, at return cur, wark, ftime, fsize, ip, at, un
return cur, None, None, None, "", None return cur, None, None, None, "", None, ""
def _forget_file( def _forget_file(
self, self,
@ -5059,7 +5113,7 @@ class Up2k(object):
"wb", "wb",
fdir=pdir, fdir=pdir,
suffix="-%.6f-%s" % (job["t0"], dip), suffix="-%.6f-%s" % (job["t0"], dip),
chmod=vf.get("chmod_f", -1), vf=vf,
) )
try: try:
abspath = djoin(pdir, job["tnam"]) abspath = djoin(pdir, job["tnam"])

View file

@ -155,7 +155,9 @@ except:
HAVE_PSUTIL = False HAVE_PSUTIL = False
try: try:
if os.environ.get("PRTY_NO_MAGIC"): if os.environ.get("PRTY_NO_MAGIC") or (
ANYWIN and not os.environ.get("PRTY_FORCE_MAGIC")
):
raise Exception() raise Exception()
import magic import magic
@ -241,7 +243,18 @@ except:
BITNESS = struct.calcsize("P") * 8 BITNESS = struct.calcsize("P") * 8
ansi_re = re.compile("\033\\[[^mK]*[mK]") RE_ANSI = re.compile("\033\\[[^mK]*[mK]")
RE_HTML_SH = re.compile(r"[<>&$?`\"';]")
RE_CTYPE = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
RE_CDISP = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
RE_CDISP_FIELD = re.compile(
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
RE_CDISP_FILE = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
RE_MEMTOTAL = re.compile("^MemTotal:.* kB")
RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
BOS_SEP = ("%s" % (os.sep,)).encode("ascii") BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@ -386,6 +399,9 @@ application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=v
text ass=plain ssa=plain text ass=plain ssa=plain
image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu
image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml image heic=heic-sequence heif=heif-sequence hdr=vnd.radiance svg=svg+xml
image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf
image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf
image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f
audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp audio caf=x-caf mp3=mpeg m4a=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp
video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t
video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr
@ -486,11 +502,11 @@ def read_ram() -> tuple[float, float]:
with open("/proc/meminfo", "rb", 0x10000) as f: with open("/proc/meminfo", "rb", 0x10000) as f:
zsl = f.read(0x10000).decode("ascii", "replace").split("\n") zsl = f.read(0x10000).decode("ascii", "replace").split("\n")
p = re.compile("^MemTotal:.* kB") p = RE_MEMTOTAL
zs = next((x for x in zsl if p.match(x))) zs = next((x for x in zsl if p.match(x)))
a = int((int(zs.split()[1]) / 0x100000) * 100) / 100 a = int((int(zs.split()[1]) / 0x100000) * 100) / 100
p = re.compile("^MemAvailable:.* kB") p = RE_MEMAVAIL
zs = next((x for x in zsl if p.match(x))) zs = next((x for x in zsl if p.match(x)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100 b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except: except:
@ -1585,7 +1601,8 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fun = kwargs.pop("fun", open) fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None) fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None) suffix = kwargs.pop("suffix", None)
chmod = kwargs.pop("chmod", -1) vf = kwargs.pop("vf", None)
fperms = vf and "fperms" in vf
if fname == os.devnull: if fname == os.devnull:
return fun(fname, *args, **kwargs), fname return fun(fname, *args, **kwargs), fname
@ -1629,11 +1646,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fp2 = os.path.join(fdir, fp2) fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2: with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8")) f2.write(orig_name.encode("utf-8"))
if chmod >= 0: if fperms:
os.fchmod(f2.fileno(), chmod) set_fperms(f2, vf)
if chmod >= 0: if fperms:
os.fchmod(f.fileno(), chmod) set_fperms(f, vf)
return f, fname return f, fname
@ -1695,14 +1712,10 @@ class MultipartParser(object):
self.args = args self.args = args
self.headers = http_headers self.headers = http_headers
self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE) self.re_ctype = RE_CTYPE
self.re_cdisp = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE) self.re_cdisp = RE_CDISP
self.re_cdisp_field = re.compile( self.re_cdisp_field = RE_CDISP_FIELD
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE self.re_cdisp_file = RE_CDISP_FILE
)
self.re_cdisp_file = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
self.boundary = b"" self.boundary = b""
self.gen: Optional[ self.gen: Optional[
@ -2037,15 +2050,25 @@ def formatdate(ts: Optional[float] = None) -> str:
return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s) return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s)
def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") -> str: def gencookie(
k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = ""
) -> str:
v = v.replace("%", "%25").replace(";", "%3B") v = v.replace("%", "%25").replace(";", "%3B")
if dur: if dur:
exp = formatdate(time.time() + dur) exp = formatdate(time.time() + dur)
else: else:
exp = "Fri, 15 Aug 1997 01:00:00 GMT" exp = "Fri, 15 Aug 1997 01:00:00 GMT"
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax" t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s"
return t % (k, v, r, exp, "; Secure" if tls else "", txt) return t % (
k,
v,
r,
exp,
"; Secure" if tls else "",
txt,
"Lax" if lax else "Strict",
)
def humansize(sz: float, terse: bool = False) -> str: def humansize(sz: float, terse: bool = False) -> str:
@ -2234,6 +2257,16 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:
return ret return ret
def html_sh_esc(s: str) -> str:
s = re.sub(RE_HTML_SH, "_", s).replace(" ", "%20")
s = s.replace("\r", "_").replace("\n", "_")
return s
def json_hesc(s: str) -> str:
return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str: def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str:
"""html.escape but also newlines""" """html.escape but also newlines"""
s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@ -2366,6 +2399,21 @@ def ujoin(rd: str, fn: str) -> str:
return rd or fn return rd or fn
def str_anchor(txt) -> tuple[int, str]:
if not txt:
return 0, ""
txt = txt.lower()
a = txt.startswith("^")
b = txt.endswith("$")
if not b:
if not a:
return 1, txt # ~
return 2, txt[1:] # ^
if not a:
return 3, txt[:-1] # $
return 4, txt[1:-1] # ^$
def log_reloc( def log_reloc(
log: "NamedLogger", log: "NamedLogger",
re: dict[str, str], re: dict[str, str],
@ -2553,6 +2601,14 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
log("lsof failed; " + min_ex(), 3) log("lsof failed; " + min_ex(), 3)
def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> None:
fno = f.fileno()
if "chmod_f" in vf:
os.fchmod(fno, vf["chmod_f"])
if "chown" in vf:
os.fchown(fno, vf["uid"], vf["gid"])
def _fs_mvrm( def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool: ) -> bool:
@ -2652,7 +2708,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
return _fs_mvrm(log, abspath, "", False, flags) return _fs_mvrm(log, abspath, "", False, flags)
def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str]: def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
try: try:
ap = fsenc(abspath) ap = fsenc(abspath)
while prune and not os.path.isdir(ap) and BOS_SEP in ap: while prune and not os.path.isdir(ap) and BOS_SEP in ap:
@ -2663,17 +2719,22 @@ def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str
assert ctypes # type: ignore # !rm assert ctypes # type: ignore # !rm
abspath = fsdec(ap) abspath = fsdec(ap)
bfree = ctypes.c_ulonglong(0) bfree = ctypes.c_ulonglong(0)
btotal = ctypes.c_ulonglong(0)
bavail = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree) ctypes.c_wchar_p(abspath),
ctypes.pointer(bavail),
ctypes.pointer(btotal),
ctypes.pointer(bfree),
) )
return (bfree.value, None, "") return (bavail.value, btotal.value, "")
else: else:
sv = os.statvfs(ap) sv = os.statvfs(ap)
free = sv.f_frsize * sv.f_bfree free = sv.f_frsize * sv.f_bavail
total = sv.f_frsize * sv.f_blocks total = sv.f_frsize * sv.f_blocks
return (free, total, "") return (free, total, "")
except Exception as ex: except Exception as ex:
return (None, None, repr(ex)) return (0, 0, repr(ex))
if not ANYWIN and not MACOS: if not ANYWIN and not MACOS:
@ -2893,6 +2954,27 @@ def load_ipu(
return ip_u, nm return ip_u, nm
def load_ipr(
log: "RootLogger", iprs: list[str], defer_mutex: bool = False
) -> dict[str, NetMap]:
ret = {}
for ipr in iprs:
try:
zs, uname = ipr.split("=")
cidrs = zs.split(",")
except:
t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)"
raise Exception(t % (ipr,))
try:
nm = NetMap(["::"], cidrs, True, True, defer_mutex)
except Exception as ex:
t = "failed to translate --ipr into netmap, probably due to invalid config: %r"
log("root", t % (ex,), 1)
raise
ret[uname] = nm
return ret
def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]: def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]:
readsz = min(bufsz, 128 * 1024) readsz = min(bufsz, 128 * 1024)
with open(fsenc(fn), "rb", bufsz) as f: with open(fsenc(fn), "rb", bufsz) as f:
@ -2924,6 +3006,17 @@ def justcopy(
return tlen, "checksum-disabled", "checksum-disabled" return tlen, "checksum-disabled", "checksum-disabled"
def eol_conv(
fin: Generator[bytes, None, None], conv: str
) -> Generator[bytes, None, None]:
crlf = conv.lower() == "crlf"
for buf in fin:
buf = buf.replace(b"\r", b"")
if crlf:
buf = buf.replace(b"\n", b"\r\n")
yield buf
def hashcopy( def hashcopy(
fin: Generator[bytes, None, None], fin: Generator[bytes, None, None],
fout: Union[typing.BinaryIO, typing.IO[Any]], fout: Union[typing.BinaryIO, typing.IO[Any]],
@ -3509,7 +3602,7 @@ def runihook(
verbose: bool, verbose: bool,
cmd: str, cmd: str,
vol: "VFS", vol: "VFS",
ups: list[tuple[str, int, int, str, str, str, int]], ups: list[tuple[str, int, int, str, str, str, int, str]],
) -> bool: ) -> bool:
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
bcmd = [sfsenc(x) for x in acmd] bcmd = [sfsenc(x) for x in acmd]
@ -4154,7 +4247,12 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream) stream = codecs.getreader(enc)(stream)
return stream return stream
return open(os.path.join(E.mod, name), mode, encoding=enc) ap = os.path.join(E.mod, name)
if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore
return open(ap, mode, encoding=enc)
class Pebkac(Exception): class Pebkac(Exception):

View file

@ -48,6 +48,7 @@ window.baguetteBox = (function () {
var onFSC = function (e) { var onFSC = function (e) {
isFullscreen = !!document.fullscreenElement; isFullscreen = !!document.fullscreenElement;
clmod(document.documentElement, 'bb_fsc', isFullscreen);
}; };
var overlayClickHandler = function (e) { var overlayClickHandler = function (e) {
@ -402,7 +403,7 @@ window.baguetteBox = (function () {
if (isFullscreen) if (isFullscreen)
document.exitFullscreen(); document.exitFullscreen();
else else
(vid() || ebi('bbox-overlay')).requestFullscreen(); ebi('bbox-overlay').requestFullscreen();
} }
catch (ex) { catch (ex) {
if (IPHONE) if (IPHONE)

View file

@ -1113,18 +1113,7 @@ html.y #widget.open {
top: -.12em; top: -.12em;
} }
#wtico { #wtico {
cursor: url(dd/4.png), pointer; cursor: pointer;
animation: cursor 500ms;
}
#wtico:hover {
animation: cursor 500ms infinite;
}
@keyframes cursor {
0% {cursor: url(dd/2.png), pointer}
30% {cursor: url(dd/3.png), pointer}
50% {cursor: url(dd/4.png), pointer}
75% {cursor: url(dd/5.png), pointer}
85% {cursor: url(dd/4.png), pointer}
} }
@keyframes spin { @keyframes spin {
100% {transform: rotate(360deg)} 100% {transform: rotate(360deg)}
@ -1385,6 +1374,7 @@ html.y #ops svg circle {
#op_cfg input[type=text] { #op_cfg input[type=text] {
top: -.3em; top: -.3em;
} }
.opview select,
.opview input[type=text] { .opview input[type=text] {
color: var(--fg); color: var(--fg);
background: var(--txt-bg); background: var(--txt-bg);
@ -1395,6 +1385,11 @@ html.y #ops svg circle {
border-radius: .2em; border-radius: .2em;
padding: .2em .3em; padding: .2em .3em;
} }
.opview select {
padding: .3em;
margin: .2em .4em;
background: var(--bg-u3);
}
.opview input.err { .opview input.err {
color: var(--err-fg); color: var(--err-fg);
background: var(--err-bg); background: var(--err-bg);
@ -1936,6 +1931,11 @@ html.y #tree.nowrap .ntree a+a:hover {
padding: 0; padding: 0;
font-size: 1.5em; font-size: 1.5em;
} }
#fs_abrt {
margin-top: 1em;
text-shadow: 0;
box-shadow: 1px 1px 0 var(--bg-d3);
}
#doc { #doc {
overflow: visible; overflow: visible;
background: #fff; background: #fff;
@ -2126,6 +2126,13 @@ html.noscroll .sbar::-webkit-scrollbar {
vertical-align: middle; vertical-align: middle;
transition: transform .23s, left .23s, top .23s, width .23s, height .23s; transition: transform .23s, left .23s, top .23s, width .23s, height .23s;
} }
html.bb_fsc .full-image img,
html.bb_fsc .full-image video {
max-height: 100%;
}
html.bb_fsc figcaption {
display: none;
}
.full-image img.nt, .full-image img.nt,
.full-image video.nt { .full-image video.nt {
transition: none; transition: none;
@ -3278,3 +3285,790 @@ html.d #treepar {
transition: background-color .3s ease, color .3s ease; transition: background-color .3s ease, color .3s ease;
} }
} }
html.ey {
--negative-space: 0em; /* Use this to change the global spacing of the 95 theme */
--font-main: consolas;
--font-serif: consolas;
--font-mono: consolas;
--w: #fff;
--w2: #dfdfdf;
--w3: grey;
--fg: #000;
--fg-max: #0000ff;
--fg-weak: #0000ff;
--bg: #c6c3c6;
--bg-d3: #ff0;
--bg-d2: var(--w3);
--bg-d1: var(--bg);
--bg-u2: var(--bg);
--bg-u3: var(--bg);
--bg-u5: var(--shadow-color-2);
--tab-alt: #00f;
--g-fsel-bg: #00f;
--g-sel-bg: #00f;
--g-fsel-b1: #fff;
--row-alt: var(--w);
--scroll: var(--silver);
--f-sel-sh: transparent;
--a: #000;
--a-b: #fff;
--a-hil: #fff;
--a-h-bg: var(--bg);
--a-dark: var(--a);
--a-gray: var(--fg-weak);
--btn-fg: var(--fg);
--btn-bg: var(--bg);
--btn-h-fg: var(--fg);
--btn-h-bg: var(--bg);
--btn-1-fg: var(--fg);
--btn-1-bg: var(--bg);
--btn-1h-bg: var(--bg-d3);
--txt-sh: a;
--txt-bg: var(--white);
--u2-b1-bg: var(--w2);
--u2-b2-bg: var(--w2);
--u2-txt-bg: var(--w2);
--u2-tab-bg: a;
--u2-tab-1-bg: var(--w2);
--sort-1: var(--fg-weak);
--tree-bg: var(--w);
--g-b1: a;
--g-b2: a;
--g-f-bg: var(--w2);
--f-sh1: 0.1;
--f-sh2: 0.02;
--f-sh3: 0.1;
--f-h-b1: a;
--srv-1: var(--w);
--srv-3: var(--a);
--mp-sh: a;
--black: #000;
--white: #fff;
--grey: grey;
--silver: silver;
--transparent: transparent;
--shadow-color-1: #0a0a0a;
--shadow-color-2: #808080;
--border-dashed-black: 1px dashed var(--black);
--radius: 0;
--focus-outline: 1px dashed var(--black);
--hover-outline: 1px dotted var(--black);
--fm-off: var(--w3);
--ttlbar: linear-gradient(90deg, navy, #1084d0);
--inset-bg: var(--white);
--scroll-bkg: var(--white);
/*All sides*/
--shadow-outset: inset -1px -1px var(--shadow-color-1),
inset 1px 1px var(--white), inset -2px -2px var(--grey),
inset 2px 2px var(--w2);
--shadow-inset: inset -1px -1px var(--white),
inset 1px 1px var(--shadow-color-1), inset -2px -2px var(--w2),
inset 2px 2px var(--shadow-color-2);
--shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--grey),
inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-1);
/*Indiv sides*/
--shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),
inset 0 -2px var(--grey);
--shadow-outset-right: inset -1px 0 var(--shadow-color-1),
inset -2px 0 var(--grey);
--shadow-outset-left: inset 1px 0 var(--white), inset 2px 0 var(--w2);
--shadow-outset-top: inset 0 1px var(--white), inset 0 2px var(--w2);
--shadow-inset-bottom: inset 0 -1px var(--white), inset 0 -2px var(--w2);
--shadow-inset-right: inset -1px 0 var(--white), inset -2px 0 var(--w2);
--shadow-inset-left: inset 1px 0 var(--shadow-color-1),
inset 2px 0 var(--shadow-color-2);
--shadow-inset-top: inset 0 1px var(--shadow-color-1),
inset 0 2px var(--shadow-color-2);
}
html.ez {
--negative-space: 0em; /* Use this to change the global spacing of your theme :) */
--font-main: consolas;
--font-serif: consolas;
--font-mono: consolas;
--w: #fff;
--w2: var(--inset-bg);
--w3: grey;
--fg: #cfcfcf;
--fg-max: #47b8ff;
--fg-weak: #47b8ff;
--bg: #383838;
--bg-d3: #600000;
--bg-d2: var(--shadow-color-1);
--bg-d1: var(--bg);
--u2-tab-1-fg: #ff0;
--bg-u2: var(--bg);
--bg-u3: var(--bg);
--bg-u5: var(--shadow-color-2);
--tab-alt: #47b8ff;
--g-fsel-bg: #0000b7;
--g-sel-bg: #00f;
--g-fsel-b1: #fff;
--row-alt: #555555;
--scroll: #555555;
--f-sel-sh: transparent;
--a: var(--fg);
--a-b: var(--fg);
--a-hil: var(--fg);
--btn-1h-bg: var(--bg-d3);
--a-h-bg: var(--bg);
--a-dark: var(--a);
--a-gray: var(--fg-weak);
--btn-fg: var(--white);
--btn-bg: var(--bg);
--btn-h-fg: var(--white);
--btn-h-bg: var(--bg);
--btn-1-fg: var(--white);
--btn-1-bg: var(--bg);
--txt-sh: a;
--u2-b1-bg: var(--w2);
--u2-b2-bg: var(--w2);
--u2-txt-bg: var(--w2);
--u2-tab-bg: a;
--u2-tab-1-bg: var(--w2);
--sort-1: var(--fg-weak);
--g-b1: a;
--g-b2: a;
--g-f-bg: var(--w2);
--f-sh1: 0.1;
--f-sh2: 0.02;
--f-sh3: 0.1;
--f-h-b1: a;
--srv-1: var(--w);
--srv-3: var(--a);
--mp-sh: a;
--black: #000;
--white: #fff;
--grey: grey;
--silver: #858585;
--transparent: transparent;
--shadow-color-1: #101010;
--shadow-color-2: #1f1f1f;
--border-dashed-black: 1px dashed var(--shadow-color-1);
--radius: 0;
--focus-outline: 1px dashed var(--white);
--hover-outline: 1px dotted var(--white);
--fm-off: var(--w3);
--ttlbar: linear-gradient(90deg, var(--shadow-color-1) 20%, #888888);
--inset-bg: #3f3f3f;
--tree-bg: var(--inset-bg);
--txt-bg: var(--inset-bg);
--scroll-bkg: var(--black);
/*All sides*/
--shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px #878787,
inset -2px -2px var(--shadow-color-2), inset 2px 2px #575757;
--shadow-inset: inset -1px -1px #878787, inset 1px 1px var(--shadow-color-1),
inset -2px -2px #575757, inset 2px 2px var(--shadow-color-2);
--shadow-input: inset -1px -1px var(--white),
inset 1px 1px var(--shadow-color-2), inset -2px -2px #575757,
inset 2px 2px var(--shadow-color-1);
--shadow-outset-bottom: inset 0 -1px var(--shadow-color-1),
inset 0 -2px var(--shadow-color-2);
--shadow-outset-right: inset -1px 0 var(--shadow-color-1),
inset -2px 0 var(--shadow-color-2);
--shadow-outset-left: inset 1px 0 #878787, inset 2px 0 #575757;
--shadow-outset-top: inset 0 1px #878787, inset 0 2px #575757;
--shadow-inset-bottom: inset 0 -1px #878787, inset 0 -2px #575757;
--shadow-inset-right: inset -1px 0 #878787, inset -2px 0 #575757;
--shadow-inset-left: inset 1px 0 var(--shadow-color-1),
inset 2px 0 var(--shadow-color-2);
--shadow-inset-top: inset 0 1px var(--shadow-color-1),
inset 0 2px var(--shadow-color-2);
}
html.e {
text-shadow: none;
}
html.e #files,
html.e #u2conf input[type="checkbox"]:hover + label,
html.e .tgl.btn.on:hover,
html.e body {
background: var(--bg);
}
html.e #pctl a,
html.e #repl,
html.e #u2conf a,
html.e #u2conf input[type="checkbox"] + label,
html.e #wfp a,
html.e .btn,
html.e .eq_step,
html.e input[type="submit"] {
box-shadow: var(--shadow-outset);
border-radius: var(--radius);
background: var(--bg);
border: 0;
}
a.s0r,
html.e #ghead a.s0,
html.e #u2conf input[type="checkbox"]:checked + label,
html.e .tgl.btn.on,
html.e input[type="submit"]:active {
box-shadow: var(--shadow-inset) !important;
}
html.e #ops a:hover,
html.e #pctl a:hover,
html.e #repl:hover,
html.e #u2conf a:hover,
html.e #u2conf input[type="checkbox"]:hover + label,
html.e #wfp a:hover,
html.e .btn:hover,
html.e .eq_step:hover,
html.e input[type="submit"]:hover {
outline: var(--hover-outline);
outline-offset: -4px;
}
html.e .ntree a:hover,
html.e :focus,
html.e :focus + label,
html.e a:active,
html.e tr:focus,
input[type="text"]:focus {
outline: var(--focus-outline) !important;
}
html.e tr:focus {
box-shadow: none;
}
html.e #pctl a:focus,
html.e #repl:hover,
html.e #u2conf input[type="checkbox"]:focus + label,
html.e #wfp a:focus,
html.e .btn:focus,
html.e .eq_step:focus {
border: 0 !important;
outline: var(--focus-outline) !important;
outline-offset: 2px;
box-shadow: var(--shadow-outset) !important;
}
html.e #files tbody,
html.e #u2cards a.act {
box-shadow: var(--shadow-inset);
}
html.e #files {
border: 2px groove var(--transparent);
box-sizing: border-box;
width: 100%;
padding: 0.3em;
top: 0;
border: 0;
}
html.e #files tbody tr td,
html.e #files thead th {
border-radius: var(--radius);
}
#files td {
background: var(--w2);
}
html.e #files tr {
background-color: var(--black);
}
html.e #srv_info span,
html.e label {
color: var(--btn-fg) !important;
}
html.e #acc_info {
background: var(--transparent);
color: var(--white);
height: 2em;
left: 1em;
width: fit-content;
}
html.e #acc_info,
html.e #ops,
html.e #srv_info {
display: flex;
align-items: center;
}
html.e #flogout:before {
padding-left: 0.2em;
padding-right: 0.4em;
content: " | ";
}
html.e #blogout {
color: var(--w);
box-shadow: none;
background: transparent;
}
html.e .opwide > div {
border-left: 1px solid var(--fg);
}
html.e #srv_info {
background: var(--transparent);
color: var(--white);
height: fit-content;
top: 3.2em;
left: 1em;
gap: 0.2em;
}
html.e #u2cards a.act {
padding: 0.2em 1em;
}
html.e #u2btn {
border: var(--border-dashed-black);
border-radius: var(--border-radius);
transform: translateY(30%);
}
html.e #ops,
html.e #ops a {
border-radius: var(--radius);
}
@media only screen and (max-width: 600px) {
html.e #acc_info {
background: var(--transparent);
color: var(--white);
height: fit-content;
align-items: center;
top: 3.2em;
right: 1em;
left: auto;
display: flex;
gap: 0.2em;
}
html.e #u2btn {
transform: none;
}
}
html.e #ops {
background: var(--ttlbar);
/*HC*/
box-shadow: inset 0-1px grey, inset 0-2px var(--shadow-color-1);
height: 2em;
gap: 0.6em;
padding: 0.2em;
flex-direction: row-reverse;
margin-bottom: 1.2em;
}
html.e #srch_form,
html.e .opbox {
padding-bottom: 1em;
padding-top: 1em;
max-width: 100vw;
}
html.e #ghead,
html.e #ops a {
align-items: center;
display: flex;
}
html.e #ops a {
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
height: 1.4em;
padding: 0;
box-shadow: var(--shadow-outset);
background: var(--bg);
aspect-ratio: 1/1;
justify-content: center;
font-size: 1.25em;
z-index: 4;
}
html.e #blogout:focus,
html.e #ops a:focus {
outline: 1px dashed var(--w) !important;
}
html.e #blogout:hover {
text-decoration: underline;
}
html.e #ops > a:not(:first-child).act {
height: 1.4em;
width: 1.4em;
padding-bottom: 0.3em;
margin-top: 0.3em;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
box-shadow: var(--shadow-inset-left), var(--shadow-inset-top),
var(--shadow-inset-right);
z-index: 6;
}
html.e #ops a.act {
box-shadow: var(--shadow-inset);
border-bottom: 0;
}
html.e a:active {
border: 0;
}
html.e :focus,
html.e :focus + label {
border: 0 !important;
outline-offset: 1px;
border-radius: var(--radius) !important;
box-shadow: inherit;
}
html.e #opa_x {
text-shadow: 0 0 0 var(--transparent) !important;
color: var(--bg) !important;
display: flex;
}
html.e #opa_x:before {
content: "";
color: var(--fg) !important;
margin-top: -0.1em;
font-size: 1.75em;
position: absolute;
}
html.e .opbox {
margin: -1.2em 0 0;
box-shadow: var(--shadow-inset-bottom), var(--shadow-inset-left),
var(--shadow-inset-right);
border-radius: var(--radius);
z-index: 5;
background: var(--bg);
}
html.e #srch_form {
margin: 0;
border-radius: var(--radius);
}
html.e #op_unpost {
max-width: 100vw;
margin: 0;
}
html.e label:focus {
box-shadow: 0 0;
}
html.e #tree {
box-shadow: none;
padding-right: 5px;
}
html.e #tt {
background: var(--w2);
}
html.e .mdo a {
background: 0 0;
text-decoration: underline;
}
html.e .mdo code,
html.e .mdo pre {
color: var(--white);
background: var(--w2);
border: 0;
}
html.e .mdo h1,
html.e .mdo h2 {
background: 0 0;
border-color: var(--w2);
}
html.e #tt,
html.e .mdo ol ol,
html.e .mdo ol ul,
html.e .mdo ul ol,
html.e .mdo ul ul {
border-color: var(--w2);
}
html.e .mdo li > em,
html.e .mdo p > em,
html.e .mdo td > em {
color: #fd0;
}
html.e input.txtbox,
html.e input[type="text"],
html.e select {
background-color: var(--txt-bg);
box-shadow: var(--shadow-input) !important;
box-sizing: border-box;
padding: 3px 4px;
border-radius: var(--radius);
border: 0;
}
html.e #gfiles {
box-shadow: var(--shadow-outset);
background: var(--bg);
padding: 0.4em;
display: flex;
flex-direction: column;
gap: 0.3em;
}
html.e #ggrid {
background-color: var(--inset-bg);
box-shadow: var(--shadow-input);
padding: 1.5em;
margin: 0;
overflow-x: scroll;
}
html.e #ghead {
margin: 0;
justify-content: flex-end;
gap: 0.4em;
padding: 0;
overflow: auto;
top: 0px;
border-radius: 0px;
}
html.e #ghead a {
margin: 0;
border-radius: var(--radius);
}
html.e ::-webkit-scrollbar,
html.e::-webkit-scrollbar {
width: 16px !important;
height: 16px !important;
background: var(--transparent) !important;
}
html.e ::-webkit-scrollbar-button,
html.e ::-webkit-scrollbar-thumb,
html.e::-webkit-scrollbar-button,
html.e::-webkit-scrollbar-thumb {
width: 16px !important;
height: 16px !important;
background: var(--scroll) !important;
/*HC*/
box-shadow: var(--shadow-outset);
border: 1px solid !important;
border-color: var(--silver) var(--black) var(--black) var(--silver) !important;
}
html.e ::-webkit-scrollbar-track,
html.e::-webkit-scrollbar-track {
image-rendering: optimize-contrast !important;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAyIDIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxtZXRhZGF0YT5NYWRlIHdpdGggUGl4ZWxzIHRvIFN2ZyBodHRwczovL2NvZGVwZW4uaW8vc2hzaGF3L3Blbi9YYnh2Tmo8L21ldGFkYXRhPgo8cGF0aCBzdHJva2U9IiNjMGMwYzAiIGQ9Ik0wIDBoMU0xIDFoMSIgLz4KPC9zdmc+) !important;
background-position: 0 0 !important;
background-repeat: repeat !important;
background-size: 2px !important;
background: var(--scroll-bkg);
}
#tree::-webkit-scrollbar,
#tree::-webkit-scrollbar-track {
background: var(--scroll-bkg);
}
html.e ::-webkit-scrollbar-button,
html.e::-webkit-scrollbar-button {
background-repeat: no-repeat !important;
background-size: 16px !important;
}
html.e ::-webkit-scrollbar-button:single-button:vertical:decrement,
html.e::-webkit-scrollbar-button:single-button:vertical:decrement {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTcgNWgxTTYgNmgzTTUgN2g1TTQgOGg3IiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-button:single-button:vertical:increment,
html.e::-webkit-scrollbar-button:single-button:vertical:increment {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNWg3TTUgNmg1TTYgN2gzTTcgOGgxIiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-button:single-button:horizontal:decrement,
html.e::-webkit-scrollbar-button:single-button:horizontal:decrement {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTggM2gxTTcgNGgyTTYgNWgzTTUgNmg0TTYgN2gzTTcgOGgyTTggOWgxIiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-button:single-button:horizontal:increment,
html.e::-webkit-scrollbar-button:single-button:horizontal:increment {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTYgM2gxTTYgNGgyTTYgNWgzTTYgNmg0TTYgN2gzTTYgOGgyTTYgOWgxIiAvPgo8L3N2Zz4=) !important;
}
html.e ::-webkit-scrollbar-corner,
html.e::-webkit-scrollbar-corner {
background: var(--silver) !important;
}
html,
html.e #tree {
scrollbar-color: inherit !important;
}
html.e #tree {
background: var(--bg);
padding-left: 0.4em;
padding-top: 0;
margin-left: var(--negative-space);
}
html.e.noscroll #tree {
/*HC*/
box-shadow: 1px 1px var(--grey), 2px 2px var(--shadow-color-1),
var(--shadow-outset-bottom);
}
html.e #treeh {
background: var(--bg);
box-shadow: var(--shadow-outset-top), var(--shadow-outset-bottom);
width: calc(1.5em + var(--nav-sz) - var(--sbw));
height: 2.4em;
border: none;
top: -2px;
display: flex;
align-items: center;
gap: 0.6em;
}
html.e #treeh .btn {
margin: 0px;
top: auto;
}
html.e #tree ul {
border-left: var(--border-dashed-black);
margin-left: 2.15em;
}
html.e .ntree a:first-child {
font-family: scp, monospace, monospace;
font-size: 1.2em;
line-height: 0;
background: var(--inset-bg);
aspect-ratio: 1/1;
text-align: center;
align-content: center;
border-radius: var(--radius) !important;
padding: 0.057em;
border: 1px solid var(--black);
}
html.e .ntree a:first-child:after {
content: ".";
position: absolute;
border-top: var(--border-dashed-black);
color: var(--transparent);
font-size: 0.9em;
margin-left: 0.13em;
}
html.e #treeul {
border: 0 !important;
position: static;
margin: 0 !important;
min-height: 100%;
height: max-content;
}
html.e .ntree a:last-of-type:before {
content: "📁";
margin-left: 0.3em;
}
html.e .ntree {
padding-left: 1em !important;
padding-top: 0.3em !important;
background: var(--inset-bg);
box-shadow: var(--shadow-inset-left), var(--shadow-inset-bottom);
}
html.e #tree li {
margin-left: -0.5em;
border-top: 0;
}
html.e .ntree a:hover {
outline-offset: -2px;
color: var(--fg);
border-radius: var(--radius) !important;
}
html.e #treepar {
width: calc(-1em + var(--nav-sz) - var(--sbw));
overflow: hidden;
left: -0.7em;
box-shadow: var(--shadow-inset-left), var(--shadow-inset-top);
border-left: 0 !important;
border-bottom: var(--border-dashed-black);
margin-left: calc(2.1em - (1em - var(--negative-space))) !important;
}
html.e #path,
html.e #widgeti,
html.e #wtoggle,
html.e #wtoggle a,
html.e #files,
html.e #files thead th,
html.e #ghead a,
html.e #tree {
box-shadow: var(--shadow-outset);
}
html.e.noscroll #treepar {
width: calc(var(--nav-sz) - 1em);
}
html.e #docul {
border-left: 0 !important;
margin-left: 0 !important;
}
html.e #wrap {
transform: translateX(calc((var(--negative-space) * 2) - 1.2em));
padding-right: var(--negative-space);
position: relative;
margin-right: calc((var(--negative-space) * 2) - 1.2em);
margin-top: var(--negative-space);
margin-left: 1.2em;
/*overflow-x: auto; fix for OOB table when screen space is limited (mobile), but removes sticky header*/
}
html.e input[type="radio"] {
accent-color: #232323;
}
html.e #path {
width: calc(100% - 0.4em);
display: flex;
align-items: center;
margin: 0;
padding: 0.2em;
overflow-x: auto;
}
html.e #path i {
border: 1px solid var(--w);
border-color: var(--w);
margin: 0;
border-width: 0.1em 0.1em 0 0;
height: 0.5em;
width: 0.5em;
}/*
html.e #hovertree:after {
color: red;
content: "BUGGY";
html.ez #hovertree:after {
color: rgb(255 98 98);
content: "BUGGY";
}
}*/
html.e #widget {
box-shadow: 0 0;
border: 0 !important;
}
html.e #wtico,
html.e #zip1 {
box-shadow: 0 0 !important;
}
html.e #wtgrid {
top: -0.09em;
}
html.e #wfs,
html.e #wm3u,
html.e #wnp,
html.e #wzip {
border-width: 0 1px 0 0;
}
html.e #wfm.act + #wzip1 + #wzip,
html.e #wfm.act + #wzip1 + #wzip + #wnp {
border-left-width: 1px;
}
html.e #barpos {
/* border-radius: var(--radius); */
box-shadow: var(--shadow-inset);
}
html.e #goh + span {
border-left: 0.1em solid var(--bg-u5);
}
html.e #wfp {
margin: var(--negative-space);
font-size: 0;
display: inline-block;
}
html.e #wfp a {
font-size: large;
display: inline-block;
}
html.e #repl {
font-size: large;
padding: 0.33em;
right: calc(var(--negative-space) * 0.89);
position: absolute;
}
html.e #epi {
text-align: center;
text-wrap-mode: nowrap;
margin: 0px;
}
html.e #epi.logue:not(.mdo) {
padding: 0.8em;
box-shadow: var(--shadow-outset);
}
html.e #epi.logue.mdo {
padding-left: 3px;
}
html.e #doc {
box-shadow: var(--shadow-inset);
background: var(--inset-bg);
margin: 0.2em;
border-radius: var(--radius);
}
html.e #detree {
padding: 0px;
}

View file

@ -109,7 +109,7 @@
{%- for f in files %} {%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td> <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
{%- if f.tags is defined %} {%- if f.tags is defined %}
{%- for k in taglist %}<td>{{ f.tags[k] }}</td>{%- endfor %} {%- for k in taglist %}<td>{{ f.tags[k]|e }}</td>{%- endfor %}
{%- endif %}<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr> {%- endif %}<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %} {%- endfor %}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

View file

@ -255,7 +255,7 @@ function Modpoll() {
} }
console.log('modpoll...'); console.log('modpoll...');
var url = (document.location + '').split('?')[0] + '?_=' + Date.now(); var url = (location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -346,7 +346,7 @@ function save(e) {
fd.append("lastmod", (force ? -1 : last_modified)); fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt); fd.append("body", txt);
var url = (document.location + '').split('?')[0]; var url = (location + '').split('?')[0];
var xhr = new XHR(); var xhr = new XHR();
xhr.open('POST', url, true); xhr.open('POST', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -404,7 +404,7 @@ function save_cb() {
function run_savechk(lastmod, txt, btn, ntry) { function run_savechk(lastmod, txt, btn, ntry) {
// download the saved doc from the server and compare // download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?_=' + Date.now(); var url = (location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';

View file

@ -6,7 +6,7 @@ var dom_doc = ebi('m');
var dom_md = ebi('mt'); var dom_md = ebi('mt');
(function () { (function () {
var n = document.location + ''; var n = location + '';
n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/'); n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/');
n[0] = 'top'; n[0] = 'top';
var loc = []; var loc = [];
@ -113,7 +113,7 @@ function save(mde) {
fd.append("lastmod", (force ? -1 : last_modified)); fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt); fd.append("body", txt);
var url = (document.location + '').split('?')[0]; var url = (location + '').split('?')[0];
var xhr = new XHR(); var xhr = new XHR();
xhr.open('POST', url, true); xhr.open('POST', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@ -166,7 +166,7 @@ function save_cb() {
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512); //alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
// download the saved doc from the server and compare // download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?_=' + Date.now(); var url = (location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.responseType = 'text'; xhr.responseType = 'text';

View file

@ -19,20 +19,14 @@
<a href="{{ r }}/?h">control-panel</a> <a href="{{ r }}/?h">control-panel</a>
&nbsp; Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" /> &nbsp; Filter: <input type="text" id="filter" size="20" placeholder="documents/passwords" />
&nbsp; <span id="hits"></span> &nbsp; <span id="hits"></span>
<table id="tab"><thead><tr> <div id="tw"></div>
<th>size</th>
<th>who</th>
<th>when</th>
<th>age</th>
<th>dir</th>
<th>file</th>
</tr></thead><tbody id="tb"></tbody></table>
</div> </div>
<a href="#" id="repl">π</a> <a href="#" id="repl">π</a>
<script> <script>
var SR="{{ r }}", var SR="{{ r }}",
lang="{{ lang }}", lang="{{ lang }}",
dutc={{ this.args.js_utc }},
dfavico="{{ favico }}"; dfavico="{{ favico }}";
var STG = window.localStorage; var STG = window.localStorage;

View file

@ -1,5 +1,6 @@
function render() { function render() {
var ups = V.ups, now = V.now, html = []; var html = ['<table id="tab"><thead><tr><th>size</th><th>who</th><th>ip</th><th>when</th><th>age</th><th>dir</th><th>file</th></tr></thead><tbody>'];
var ups = V.ups, now = V.now;
ebi('filter').value = V.filter; ebi('filter').value = V.filter;
ebi('hits').innerHTML = 'showing ' + ups.length + ' files'; ebi('hits').innerHTML = 'showing ' + ups.length + ' files';
@ -10,11 +11,12 @@ function render() {
fn = esc(uricom_dec(vsp[1])), fn = esc(uricom_dec(vsp[1])),
at = f.at, at = f.at,
td = now - f.at, td = now - f.at,
ts = !at ? '(?)' : unix2iso(at), ts = !at ? '(?)' : unix2ui(at),
sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'), sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'),
sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " "); sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
html.push('<tr><td>' + sz + html.push('<tr><td>' + sz +
'</td><td>' + (f.un || '') +
'</td><td>' + f.ip + '</td><td>' + f.ip +
'</td><td>' + ts + '</td><td>' + ts +
'</td><td>' + sa + '</td><td>' + sa +
@ -26,7 +28,8 @@ function render() {
var t = V.filter ? ' matching the filter' : ''; var t = V.filter ? ' matching the filter' : '';
html = ['<tr><td colspan="6">there are no uploads' + t + '</td></tr>']; html = ['<tr><td colspan="6">there are no uploads' + t + '</td></tr>'];
} }
ebi('tb').innerHTML = html.join(''); html.push('</tbody></table>');
ebi('tw').innerHTML = html.join('\n');
} }
render(); render();
@ -46,7 +49,7 @@ function ask(e) {
V = JSON.parse(this.responseText) V = JSON.parse(this.responseText)
} }
catch (ex) { catch (ex) {
ebi('tb').innerHTML = '<tr><td colspan="6">failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre></td></tr>'; ebi('tw').innerHTML = 'failed to decode server response as json: <pre>' + esc(this.responseText) + '</pre>';
return; return;
} }
render(); render();

View file

@ -66,6 +66,7 @@
var SR="{{ r }}", var SR="{{ r }}",
shr="{{ shr }}", shr="{{ shr }}",
lang="{{ lang }}", lang="{{ lang }}",
dutc={{ this.args.js_utc }},
dfavico="{{ favico }}"; dfavico="{{ favico }}";
var STG = window.localStorage; var STG = window.localStorage;

View file

@ -3,7 +3,7 @@ for (var a = 0; a < t.length; a++)
t[a].onclick = rm; t[a].onclick = rm;
function rm() { function rm() {
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm', var u = SR + '/?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')),
xhr = new XHR(); xhr = new XHR();
xhr.open('POST', u, true); xhr.open('POST', u, true);
@ -13,7 +13,7 @@ function rm() {
function bump() { function bump() {
var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'), var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'),
u = SR + shr + uricom_enc(k) + '?eshare=' + this.value, u = SR + '/?skey=' + uricom_enc(k) + '&eshare=' + this.value,
xhr = new XHR(); xhr = new XHR();
xhr.open('POST', u, true); xhr.open('POST', u, true);
@ -25,7 +25,7 @@ function cb() {
if (this.status !== 200) if (this.status !== 200)
return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText))); return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));
document.location = '?shares'; location = '?shares';
} }
function qr(e) { function qr(e) {
@ -64,7 +64,7 @@ function showqr(href) {
for (var b = 7; b < 9; b++) { for (var b = 7; b < 9; b++) {
var v = buf[ibuf++]; var v = buf[ibuf++];
tr[a].cells[b].innerHTML = tr[a].cells[b].innerHTML =
v ? unix2iso(v).replace(' ', ',&nbsp;') : 'never'; v ? unix2ui(v).replace(' ', ',&nbsp;') : 'never';
} }
for (var a = 0; a < tr.length; a++) for (var a = 0; a < tr.length; a++)

View file

@ -24,6 +24,7 @@ h1 {
li { li {
margin: 1em 0; margin: 1em 0;
} }
#lo,
a { a {
color: #047; color: #047;
background: #fff; background: #fff;
@ -47,6 +48,7 @@ td a {
float: right; float: right;
margin: -.2em 0 0 .8em; margin: -.2em 0 0 .8em;
} }
#lo,
.logout, .logout,
a.r { a.r {
color: #c04; color: #c04;
@ -176,12 +178,14 @@ html.z {
html.z h1 { html.z h1 {
border-color: #777; border-color: #777;
} }
html.z #lo,
html.z a { html.z a {
color: #fff; color: #fff;
background: #057; background: #057;
border-color: #37a; border-color: #37a;
} }
html.z .logout, html.z .logout,
html.z #lo,
html.z a.r { html.z a.r {
background: #804; background: #804;
border-color: #c28; border-color: #c28;

View file

@ -15,14 +15,14 @@
<body> <body>
<div id="wrap"> <div id="wrap">
{%- if not in_shr %} {%- if not in_shr %}
<a id="a" href="{{ r }}/?h" class="af">refresh</a> <a id="a" href="{{ r }}/?h{{ re }}" class="af">refresh</a>
<a id="v" href="{{ r }}/?hc" class="af">connect</a> <a id="v" href="{{ r }}/?hc" class="af">connect</a>
{%- if this.uname == '*' %} {%- if this.uname == '*' %}
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p> <p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %} {%- else %}
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a> <a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
<p><span id="m">welcome back,</span> <strong>{{ this.uname|e }}</strong></p> <p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
{%- endif %} {%- endif %}
{%- endif %} {%- endif %}
@ -120,7 +120,12 @@
<div> <div>
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}"> <form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" /> <input type="hidden" id="la" name="act" value="login" />
{% if this.args.usernames %}
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
{% else %}
<input type="password" id="lp" name="cppwd" placeholder=" password" /> <input type="password" id="lp" name="cppwd" placeholder=" password" />
{% endif %}
<input type="hidden" name="uhash" id="uhash" value="x" /> <input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="login" /> <input type="submit" id="ls" value="login" />
{% if chpw %} {% if chpw %}
@ -163,6 +168,13 @@
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li> <li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li> <li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
{%- if this.uname != '*' %}
<li><form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form></li>
{% endif %}
</ul> </ul>
</div> </div>

View file

@ -1,3 +1,5 @@
// please add translations in alphabetic order, but keep "nor" and "eng" first
// (lines ending with //m are machine translations)
var Ls = { var Ls = {
"nor": { "nor": {
"a1": "oppdater", "a1": "oppdater",
@ -15,6 +17,11 @@ var Ls = {
"j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig", "j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig",
"k1": "nullstill innstillinger", "k1": "nullstill innstillinger",
"l1": "logg inn:", "l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukernavn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutter økten på alle nettlesere",
"m1": "velkommen tilbake,", "m1": "velkommen tilbake,",
"n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌", "n1": "404: filen finnes ikke &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>', "o1": 'eller kanskje du ikke har tilgang? prøv et passord eller <a href="' + SR + '/?h">gå hjem</a>',
@ -44,13 +51,13 @@ var Ls = {
"eng": { "eng": {
"d2": "shows the state of all active threads", "d2": "shows the state of all active threads",
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect", "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect",
"lo2": "ends the session on all browsers",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds", "u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v2": "use this server as a local HDD", "v2": "use this server as a local HDD",
"ta1": "fill in your new password first", "ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:", "ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again", "ta3": "found a typo; please try again",
}, },
"chi": { "chi": {
"a1": "更新", "a1": "更新",
"b1": "你好 &nbsp; <small>(你尚未登录)</small>", "b1": "你好 &nbsp; <small>(你尚未登录)</small>",
@ -67,6 +74,11 @@ var Ls = {
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。", "j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
"k1": "重置设置", "k1": "重置设置",
"l1": "登录:", "l1": "登录:",
"ls3": "登录", //m
"lu4": "用户名", //m
"lp4": "密码", //m
"lo3": "在所有地方注销 {0}", //m
"lo2": "这将结束在所有浏览器中的会话", //m
"m1": "欢迎回来,", "m1": "欢迎回来,",
"n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌", "n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌",
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>', "o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
@ -92,7 +104,661 @@ var Ls = {
"ae1": "正在下载:", //m "ae1": "正在下载:", //m
"af1": "显示最近上传的文件", //m "af1": "显示最近上传的文件", //m
"ag1": "查看已知 IdP 用户", //m "ag1": "查看已知 IdP 用户", //m
} },
"cze": {
"a1": "obnovit",
"b1": "ahoj cizinče &nbsp; <small>(nejsi přihlášen)</small>",
"c1": "odhlásit se",
"d1": "vypsat zásobníku",
"d2": "zobrazit stav všech aktivních vláken",
"e1": "znovu načíst konfiguraci",
"e2": "znovu načíst konfigurační soubory (accounts/volumes/volflags),$Na prohledat všechny e2ds úložiště$N$Npoznámka: všechny změny globálních nastavení$Nvyžadují úplné restartování, aby se projevily",
"f1": "můžeš procházet:",
"g1": "můžeš nahrávat do:",
"cc1": "další věci:",
"h1": "zakázat k304",
"i1": "povolit k304",
"j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), <em>ale</em> také to obecně zpomalí věci",
"k1": "resetovat nastavení klienta",
"l1": "přihlaste se pro více:",
"ls3": "přihlásit se", //m
"lu4": "uživatelské jméno", //m
"lp4": "heslo", //m
"lo3": "odhlásit “{0}” všude", //m
"lo2": "tímto ukončíte relaci ve všech prohlížečích", //m
"m1": "vítej zpět,",
"n1": "404 nenalezeno &nbsp;┐( ´ -`)┌",
"o1": 'nebo možná nemáš přístup -- zkus heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"p1": "403 zakázáno &nbsp;~┻━┻",
"q1": 'použij heslo nebo <a href="' + SR + '/?h">jdi domů</a>',
"r1": "jdi domů",
".s1": "znovu prohledat",
"t1": "akce",
"u2": "čas od posledního zápisu na server$N( upload / rename / ... )$N$N17d = 17 dní$N1h23 = 1 hodina 23 minut$N4m56 = 4 minuty 56 sekund",
"v1": "připojit",
"v2": "použít tento server jako místní HDD",
"w1": "přepnout na https",
"x1": "změnit heslo",
"y1": "upravit sdílení",
"z1": "odblokovat toto sdílení:",
"ta1": "nejprve vyplňte své nové heslo",
"ta2": "zopakujte pro potvrzení nového hesla:",
"ta3": "nalezen překlep; zkuste to prosím znovu",
"aa1": "příchozí soubory:",
"ab1": "deaktivovat no304",
"ac1": "povolit no304",
"ad1": "povolení no304 deaktivuje veškeré mezipaměti; zkuste to, pokud k304 nestačilo. To ovšem zapříčíní obrovské množství síťového provozu!",
"ae1": "aktivní stahování:",
"af1": "zobrazit nedávné nahrávání",
},
"deu": {
"a1": "Neu laden",
"b1": "Tach, wie geht's? &nbsp; <small>(Du bist nicht angemeldet)</small>",
"c1": "Abmelden",
"d1": "Zustand",
"d2": "Zeigt den Zustand aller aktiven Threads",
"e1": "Config neu laden",
"e2": "Konfigurationsdatei neu laden (Accounts/Volumes/VolFlags)$Nund scannt alle e2ds-Volumes$N$NBeachte: Jegliche Änderung an globalen Einstellungen$Nbenötigt einen Neustart zum Anwenden",
"f1": "Du kannst lesen:",
"g1": "Du kannst hochladen nach:",
"cc1": "Andere Dinge:",
"h1": "k304 deaktivieren",
"i1": "k304 aktivieren",
"j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer",
"k1": "Client-Einstellungen zurücksetzen",
"l1": "Melde dich an für mehr:",
"ls3": "Anmelden", //m
"lu4": "Benutzername", //m
"lp4": "Passwort", //m
"lo3": "“{0}” überall abmelden", //m
"lo2": "Dies beendet die Sitzung in allen Browsern", //m
"m1": "Willkommen zurück,",
"n1": "404 Nicht gefunden &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
"p1": "403 Verboten &nbsp;~┻━┻",
"q1": 'Benutze ein Passwort oder <a href="' + SR + '/?h">gehe zur Homepage</a>',
"r1": "Gehe zur Homepage",
".s1": "Neu scannen",
"t1": "Aktion",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v1": "Verbinden",
"v2": "Benutze diesen Server als lokale Festplatte",
"w1": "Zu HTTPS wechseln",
"x1": "Passwort ändern",
"y1": "Shares bearbeiten",
"z1": "Share entsperren:",
"ta1": "Trage zuerst dein Passwort ein",
"ta2": "Wiederhole dein Passwort zur Bestätigung:",
"ta3": "Da stimmt etwas nicht; probier's nochmal",
"aa1": "Eingehende Dateien:",
"ab1": "no304 deaktivieren",
"ac1": "no304 aktivieren",
"ad1": "Das Aktivieren von no304 deaktiviert jegliche Form von Caching; probier dies, wenn k304 nicht genug war. Dies verschwendet eine grosse Menge Netzwerk-Traffic!",
"ae1": "Aktive Downloads:",
"af1": "Zeige neue Uploads",
},
"fin": {
"a1": "päivitä",
"b1": "hei sie muukalainen &nbsp; <small>(et ole kirjautunut sisään)</small>",
"c1": "kirjaudu ulos",
"d1": "tulosta pinojälki",
"d2": "näytä kaikkien aktiivisten säikeiden tila",
"e1": "päivitä konffit",
"e2": "lataa konfiguraatiotiedostot uudelleen (käyttäjätilit/asemat/asemaflagit),$Nja skannaa kaikki e2ds asemat uudelleen$N$Nhuom: kaikki global-asetuksiin$Ntehdyt muutokset vaativat täyden$Nuudelleenkäynnistyksen",
"f1": "voit selata näitä:",
"g1": "voit ladata näihin:",
"cc1": "muuta:",
"h1": "poista k304 käytöstä",
"i1": "ota k304 käyttöön",
"j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, <em>mutta</em> se myös vähentää suorituskykyä",
"k1": "nollaa asetukset",
"l1": "kirjaudu sisään:",
"ls3": "kirjaudu sisään", //m
"lu4": "käyttäjätunnus", //m
"lp4": "salasana", //m
"lo3": "kirjaa “{0}” ulos kaikkialta", //m
"lo2": "tämä lopettaa istunnon kaikissa selaimissa", //m
"m1": "tervetuloa takaisin,",
"n1": "404: ei löytynyt mitään &nbsp;┐( ´ -`)┌",
"o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
"p1": "403: pääsy kielletty &nbsp;~┻━┻",
"q1": 'kokeile salasanaa tai <a href="' + SR + '/?h">mene kotiin</a>',
"r1": "mene kotiin",
".s1": "uudelleenkartoita",
"t1": "toiminto",
"u2": "aika viimeisestä palvelimen kirjoituksesta$N( lataus / uudelleennimeäminen / tms. )$N$N17d = 17 päivää$N1h23 = 1 tunti 23 minuuttia$N4m56 = 4 minuuttia 56 sekuntia",
"v1": "yhdistä",
"v2": "käytä tätä palvelinta paikallisena kiintolevynä",
"w1": "vaihda https:ään",
"x1": "vaihda salasana",
"y1": "muokkaa jakoja",
"z1": "avaa tämä jako:",
"ta1": "täytä ensin uusi salasana",
"ta2": "toista vahvistaaksesi uuden salasanan:",
"ta3": "löytyi kirjoitusvirhe; yritä uudelleen",
"aa1": "saapuvat:",
"ab1": "poista no304 käytöstä",
"ac1": "ota no304 käyttöön",
"ad1": "no304:n lopettaa välimuistin käytön kokonaan; kokeile tätä jos k304 ei riittänyt. Tuhlaa valtavan määrän verkkoliikennettä!",
"ae1": "lähtevät:",
"af1": "näytä viimeaikaiset lataukset",
"ag1": "näytä tunnetut IdP-käyttäjät",
},
"fra": {
"a1": "rafraîchir",
"b1": "salut étranger &nbsp; <small>(vous n'êtes pas connecté.)</small>",
"c1": "déconnexion",
"d1": "vidange de la pile",
"d2": "affiche l'état de tous les threads actifs",
"e1": "recharger la configuration",
"e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet",
"f1": "vous pouvez naviguer :",
"g1": "vous pouvez télécharger sur :",
"cc1": "autres choses :",
"h1": "désactiver k304",
"i1": "activer k304",
"j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), <em>mais</em> cela ralentira également les choses en général",
"k1": "réinitialiser les paramètres du client",
"l1": "connectez-vous pour en savoir plus :",
"ls3": "se connecter", //m
"lu4": "nom d'utilisateur", //m
"lp4": "mot de passe", //m
"lo3": "déconnecter “{0}” partout", //m
"lo2": "cela mettra fin à la session sur tous les navigateurs", //m
"m1": "heureux de vous revoir,",
"n1": "404 introuvable &nbsp;┐( ´ -`)┌",
"o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"p1": "403 interdit &nbsp;~┻━┻",
"q1": 'utiliser un mot de passe ou <a href="' + SR + '/?h">aller à la page d\'accueil</a>',
"r1": "aller à la page d\'accueil",
".s1": "rescanner",
"t1": "action",
"u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes",
"v1": "connecter",
"v2": "utilisez ce serveur en tant que disque dur local",
"w1": "passer à https",
"x1": "changer mot de passe",
"y1": "modifier les partages",
"z1": "déverrouiller ce partage :",
"ta1": "entrez d'abord votre nouveau mot de passe",
"ta2": "répétez pour confirmer le nouveau mot de passe :",
"ta3": "une faute de frappe a été détectée ; veuillez réessayer.",
"aa1": "fichiers entrants :",
"ab1": "désactiver no304",
"ac1": "activer no304",
"ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !",
"ae1": "téléchargements actifs :",
"af1": "afficher les derniers téléchargements",
},
"grc": {
"a1": "ανανέωση",
"b1": "γεια σου ξένε! &nbsp; <small>(δεν είσαι συνδεδεμένος)</small>",
"c1": "αποσύνδεση",
"d1": "λίστα διεργασιών",
"d2": "εμφανίζει την κατάσταση όλων των ενεργών διεργασιών",
"e1": "επαναφόρτωση του cfg",
"e2": "φορτώνει ξανά τα αρχεία ρυθμίσεων (λογαριασμοί/τόμοι/volflags),$Nκαι κάνει επανεξέταση όλων των τόμων e2ds$N$Nσημείωση: οποιαδήποτε αλλαγή στις καθολικές ρυθμίσεις$Nαπαιτεί πλήρη επανεκκίνηση για να εφαρμοστεί",
"f1": "μπορείς να περιηγηθείς:",
"g1": "μπορείς να εκτελέσεις μεταφόρτωση σε:",
"cc1": "άλλα πράγματα:",
"h1": "απενεργοποίση k304",
"i1": "ενεργοποίηση k304",
"j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), <em>αλλά</em> θα κάνει τα πράγματα, γενικά πιο αργά",
"k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη",
"l1": "συνδέσου για περισσότερα:",
"ls3": "σύνδεση", //m
"lu4": "όνομα χρήστη", //m
"lp4": "κωδικός πρόσβασης", //m
"lo3": "αποσύνδεση του “{0}” από παντού", //m
"lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m
"m1": "καλώς ήρθες,",
"n1": "404 δεν βρέθηκε &nbsp;┐( ´ -`)┌",
"o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
"p1": "403 απαγορευμένο &nbsp;~┻━┻",
"q1": 'δοκίμασε έναν κωδικό <a href="' + SR + '/?h">πήγαινε στην αρχική</a>',
"r1": "πίσω στην αρχική",
".s1": "επανάληψη σάρωσης",
"t1": "ενέργεια",
"u2": "χρόνος από την τελευταία εγγραφή του διακομιστή$N( μεταφόρτωση / μετονομασία / ... )$N$N17d = 17 days$N1ω23 = 1 ώρα 23 λεπτά$N4λ56 = 4 λεπτά 56 δευτερόλεπτα",
"v1": "σύνδεση",
"v2": "χρησιμοποίησε αυτόν το διακομιστή σαν τοπικό δίσκο",
"w1": "εναλλαγή σε https",
"x1": "αλλαγή κωδικού",
"y1": "επεξεργασία κοινόχρηστων φακέλων",
"z1": "ξεκλείδωμα αυτού του κοινόχρηστου φακέλου:",
"ta1": "συμπλήρωσε πρώτα το νέο σου κωδικό",
"ta2": "επανέλαβε για να επιβεβαιώσεις το νέο κωδικό:",
"ta3": "βρέθηκε τυπογραφικό λάθος· δοκίμασε ξανά",
"aa1": "εισερχόμενα αρχεία:",
"ab1": "απενεργοποίηση no304",
"ac1": "ενεργοποίηση no304",
"ad1": "η ενεργοποίηση του no304 θα απενεργοποιήσει όλη την προσωρινή αποθήκευση· δοκίμασέ το αν το k304 δεν ήταν αρκετό. Προσοχή, θα σπαταλήσει τεράστιο όγκο δικτυακής κίνησης!",
"ae1": "ενεργές μεταφορτώσεις:",
"af1": "προβολή πρόσφατων μεταφορτώσεων",
},
"ita": {
"a1": "aggiorna",
"b1": "ciao &nbsp; <small>(non sei connesso)</small>",
"c1": "disconnetti",
"d1": "stato",
"d2": "mostra lo stato di tutti i thread attivi",
"e1": "ricarica configurazione",
"e2": "ricarica i file di configurazione (account/volumi/flag dei volumi),\n e riesegue la scansione di tutti i volumi e2ds.\n\nNota: qualsiasi modifica alle impostazioni globali richiede un riavvio completo per avere effetto",
"f1": "puoi visualizzare:",
"g1": "puoi caricare su:",
"cc1": "altro:",
"h1": "disattiva k304",
"i1": "attiva k304",
"j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni",
"k1": "resetta impostazioni",
"l1": "accedi:",
"ls3": "accedi", //m
"lu4": "nome utente", //m
"lp4": "password", //m
"lo3": "disconnetti “{0}” ovunque", //m
"lo2": "questo terminerà la sessione su tutti i browser", //m
"m1": "bentornato,",
"n1": "404: file non trovato &nbsp;┐( ´ -`)┌",
"o1": "oppure forse non hai accesso? prova una password o <a href=\"SR/?h\">torna alla home</a>",
"p1": "403: accesso negato &nbsp;~┻━┻",
"q1": "prova una password o <a href=\"SR/?h\">torna alla home</a>",
"r1": "torna alla home",
".s1": "mappa",
"t1": "azione",
"u2": "tempo dall'ultima scrittura sul server\n (caricamento / rinomina / ...)\n\n17d = 17 giorni\n1h23 = 1 ora 23 minuti\n4m56 = 4 minuti 56 secondi",
"v1": "connetti",
"v2": "usa questo server come un disco locale",
"w1": "passa a https",
"x1": "cambia password",
"y1": "le tue condivisioni",
"z1": "sblocca area:",
"ta1": "devi prima inserire una nuova password",
"ta2": "ripeti per confermare la nuova password:",
"ta3": "errore di digitazione; riprova",
"aa1": "in arrivo:",
"ab1": "disattiva no304",
"ac1": "attiva no304",
"ad1": "no304 disabilita completamente la cache. Se k304 non è sufficiente, prova questa opzione. Aumenterà notevolmente il consumo di dati!",
"ae1": "in uscita:",
"af1": "mostra i file caricati di recente",
"ag1": "mostra utenti IdP conosciuti"
},
"kor": {
"a1": "새로고침",
"b1": "어이 친구! 처음 보는 얼굴인데? &nbsp; <small>(로그인되어 있지 않습니다)</small>",
"c1": "로그아웃",
"d1": "스택 덤프하기",
"d2": "모든 활성 스레드의 상태를 표시합니다",
"e1": "설정 다시 불러오기",
"e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다",
"f1": "탐색 가능한 곳:",
"g1": "업로드 가능한 곳:",
"cc1": "기타 항목:",
"h1": "k304 비활성화",
"i1": "k304 활성화",
"j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, <em>대신 전반적인 속도는 느려집니다.</em>",
"k1": "클라이언트 설정 초기화",
"l1": "로그인하기:",
"ls3": "로그인", //m
"lu4": "사용자 이름", //m
"lp4": "비밀번호", //m
"lo3": "{0}을(를) 모든 곳에서 로그아웃", //m
"lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m
"m1": "또 오셨네요,",
"n1": "404 찾을 수 없음 &nbsp;┐( ´ -`)┌",
"o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"p1": "403 접근 금지 &nbsp;~┻━┻",
"q1": "비밀번호를 입력하거나 <a href=\"' + SR + '/?h\">홈으로 이동</a>하세요",
"r1": "홈으로 이동",
".s1": "다시 스캔",
"t1": "작업",
"u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초",
"v1": "연결",
"v2": "이 서버를 로컬 하드디스크처럼 사용하기",
"w1": "HTTPS로 전환",
"x1": "비밀번호 변경",
"y1": "공유 설정",
"z1": "이 공유 잠금해제:",
"ta1": "새 비밀번호를 먼저 입력하세요",
"ta2": "새 비밀번호 확인을 위해 다시 입력하세요:",
"ta3": "오타가 있습니다. 다시 시도해주세요",
"aa1": "수신 중인 파일:",
"ab1": "no304 비활성화",
"ac1": "no304 활성화",
"ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!",
"ae1": "활성 다운로드:",
"af1": "최근 업로드 보기",
"ag1": "IdP 캐시 보기"
},
"nld": {
"a1": "Update",
"b1": "Hallo, hoe gaat het met jou? &nbsp; <small>(Je bent niet ingelogd)</small>",
"c1": "Uitloggen",
"d1": "Voorwaarde",
"d2": "Toont de status van alle actieve threads",
"e1": "Configuratie opnieuw laden.",
"e2": "Leest configuratiebestanden opnieuw in$N(accounts, volumes, volumeschakelaars)$Nen brengt alle e2ds-volumes in kaart$N$Nopmerking: veranderingen in globale parameters$Nvereist een volledige herstart van de server",
"f1": "Je kan het volgende lezen:",
"g1": "Je kan naar het volgende uploaden:",
"cc1": "Schakelaars en dergelijke:",
"h1": "k304 uitschakelen",
"i1": "k304 inschakelen",
"j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk",
"k1": "Instellingen resetten",
"l1": "Inloggen:",
"ls3": "inloggen", //m
"lu4": "gebruikersnaam", //m
"lp4": "wachtwoord", //m
"lo3": "“{0}” overal afmelden", //m
"lo2": "dit zal de sessie in alle browsers beëindigen", //m
"m1": "Welkom terug,",
"n1": "404: bestand bestaat niet &nbsp;┐( ´ -`)┌",
"o1": 'of misschien heb je geen toegang? probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
"p1": "403: toegang geweigerd &nbsp;~┻━┻",
"q1": 'Probeer een wachtwoord of <a href="' + SR + '/?h">ga naar startscherm</a>',
"r1": "Ga naar startscherm",
".s1": "Kaart",
"t1": "Actie",
"u2": "Tijd sinds iemand voor het laatst naar de server schreef$N( upload / naamswijziging / ... )$N$N17d = 17 dagen$N1h23 = 1 uur 23 minuten$N4m56 = 4 minuten 56 secondes",
"v1": "Verbinden",
"v2": "Gebruik deze server als een lokale harde schijf",
"w1": "Overschakelen naar https",
"x1": "Wachtwoord wijzigen",
"y1": "Jou gedeelde items",
"z1": "Ontgrendel gebied:",
"ta1": "Je moet eerst een nieuw wachtwoord invoeren",
"ta2": "Herhaal om nieuw wachtwoord te bevestigen:",
"ta3": "Typefout gevonden; probeer het opnieuw",
"aa1": "Inkomend:",
"ab1": "Schakel nr. 304 uit",
"ac1": "Schakel nr. 304 in",
"ad1": "Nr. 304 stopt al het cachegebruik. Als k304 niet voldoende was, probeer dan deze. Vermenigvuldigt het dataverbruik.!",
"ae1": "Uitgaand:",
"af1": "Recent geüploade bestanden weergeven",
"ag1": "Bekende IdP-gebruikers weergeven",
},
"nno": {
"a1": "oppdatér",
"b1": "heisann &nbsp; <small>(du er ikkje logga inn)</small>",
"c1": "logg ut",
"d1": "tilstand",
"d2": "vis tilstanden åt alle trådar",
"e1": "last innst.",
"e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde",
"f1": "du kan sjå på:",
"g1": "du kan laste opp åt:",
"cc1": "brytarar og slikt:",
"h1": "skru av k304",
"i1": "skru på k304",
"j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukarnamn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutt økta på alle nettlesarar",
"m1": "velkomen attende,",
"n1": "404: filen finnast ikkje &nbsp;┐( ´ -`)┌",
"o1": 'eller kanskje du ikkje har høve? prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"p1": "403: tilgang nektet &nbsp;~┻━┻",
"q1": 'prøv eit passord eller <a href="' + SR + '/?h">gå heim</a>',
"r1": "gå heim",
".s1": "kartlegg",
"t1": "handling",
"u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund",
"v1": "kople åt",
"v2": "bruk denne serveren som ein lokal harddisk",
"w1": "bytt åt https",
"x1": "bytt passord",
"y1": "dine delinger",
"z1": "lås opp område:",
"ta1": "du må skrive eit nytt passord først",
"ta2": "gjenta for å stadfeste nytt passord:",
"ta3": "fant ein skrivefeil; vennligst prøv igjen",
"aa1": "innkommande:",
"ab1": "skru av no304",
"ac1": "skru på no304",
"ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplasta filer",
"ag1": "vis kjente IdP-brukarar",
},
"pol": {
"a1": "odśwież",
"b1": "witaj, nieznajomy &nbsp; <small>(nie jesteś zalogowany)</small>",
"c1": "wyloguj się",
"d1": "zrzut stosu",
"d2": "pokazuje status wszystkich aktywnych wątków",
"e1": "przeładuj konfigurację",
"e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać",
"f1": "możesz przeglądać:",
"g1": "możesz przesyłać do:",
"cc1": "inne:",
"h1": "wyłącz k304",
"i1": "włącz k304",
"j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, <em>ale</em> spowolni ogólne działanie",
"k1": "zresetuj ustawienia klienta",
"l1": "zaloguj się po więcej:",
"ls3": "zaloguj się", //m
"lu4": "nazwa użytkownika", //m
"lp4": "hasło", //m
"lo3": "wyloguj “{0}” wszędzie", //m
"lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m
"m1": "Witaj,",
"n1": "404 nie znaleziono &nbsp;┐( ´ -`)┌",
"o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"p1": "403 odmowa dostępu &nbsp;~┻━┻",
"q1": 'użyj hasła lub <a href="' + SR + '/?h">przejdź do strony głównej</a>',
"r1": "idź do strony głównej",
".s1": "przeskanuj ponownie",
"t1": "akcje",
"u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund",
"v1": "połącz",
"v2": "używaj tego serwera jako dysku lokalnego",
"w1": "przejdź na HTTPS",
"x1": "zmień hasło",
"y1": "edytuj udostępnione",
"z1": "odblokuj udostępnienie:",
"ta1": "najpierw wprowadź nowe hasło",
"ta2": "powtórz hasło dla potwierdzenia:",
"ta3": "znaleziono literówkę, spróbuj ponownie",
"aa1": "pliki przychodzące:",
"ab1": "wyłącz no304",
"ac1": "włącz no304",
"ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!",
"ae1": "trwające pobierania:",
"af1": "pokaż ostatnio przesłane pliki",
"ag1": "pokaż znanych użytkowników IdP",
},
"spa": {
"a1": "actualizar",
"b1": "hola &nbsp; <small>(no has iniciado sesión)</small>",
"c1": "cerrar sesión",
"d1": "volcar estado de la pila",
"d2": "muestra el estado de todos los hilos activos",
"e1": "recargar configuración",
"e2": "recargar archivos de configuración (cuentas/volúmenes/indicadores de vol.),$Ny reescanear todos los volúmenes e2ds$N$Nnota: cualquier cambio en la configuración global$Nrequiere un reinicio completo para surtir efecto",
"f1": "puedes explorar:",
"g1": "puedes subir a:",
"cc1": "otras cosas:",
"h1": "desactivar k304",
"i1": "activar k304",
"j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), <em>pero</em> también ralentizará las cosas en general",
"k1": "restablecer config. de cliente",
"l1": "inicia sesión para más:",
"ls3": "iniciar sesión", //m
"lu4": "nombre de usuario", //m
"lp4": "contraseña", //m
"lo3": "cerrar sesión de “{0}” en todas partes", //m
"lo2": "esto finalizará la sesión en todos los navegadores", //m
"m1": "bienvenido de nuevo,",
"n1": "404 no encontrado &nbsp;┐( ´ -`)┌",
"o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
"p1": "403 prohibido &nbsp;~┻━┻",
"q1": 'usa una contraseña o <a href=\"' + SR + '/?h\">vuelve al inicio</a>',
"r1": "ir al inicio",
".s1": "reescanear",
"t1": "acción",
"u2": "tiempo desde la última escritura en el servidor$N( subida / renombrar / ... )$N$N17d = 17 días$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
"v1": "conectar",
"v2": "usar este servidor como un disco duro local",
"w1": "cambiar a https",
"x1": "cambiar contraseña",
"y1": "editar recursos compartidos",
"z1": "desbloquear este recurso compartido:",
"ta1": "primero escribe tu nueva contraseña",
"ta2": "repite para confirmar la nueva contraseña:",
"ta3": "hay un error; por favor, inténtalo de nuevo",
"aa1": "archivos entrantes:",
"ab1": "desactivar no304",
"ac1": "activar no304",
"ad1": "activar no304 desactivará todo el almacenamiento en caché; prueba esto si k304 no fue suficiente. ¡Esto desperdiciará una gran cantidad de tráfico de red!",
"ae1": "descargas activas:",
"af1": "mostrar subidas recientes",
"ag1": "mostrar usuarios IdP conocidos"
},
"swe": {
"a1": "uppdatera",
"b1": "tjena främling &nbsp; <small>(du är inte inloggad)</small>",
"c1": "logga ut",
"d1": "dumpa stacken",
"d2": "visar tillståndet på alla aktiva trådar",
"e1": "ladda om konfig.",
"e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart",
"f1": "du kan bläddra:",
"g1": "du kan ladda upp till:",
"cc1": "annat:",
"h1": "avaktivera k304",
"i1": "aktivera k304",
"j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), <em>men</em> saker kommer också att bli långsammare i allmänhet",
"k1": "återställ klientinställningar",
"l1": "logga in för att se mer:",
"ls3": "logga in", //m
"lu4": "användarnamn", //m
"lp4": "lösenord", //m
"lo3": "logga ut “{0}” överallt", //m
"lo2": "avsluta sessionen i alla webbläsare", //m
"m1": "välkommen tillbaka,",
"n1": "404 hittades inte &nbsp;┐( ´ -`)┌",
"o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"p1": "403 nekat &nbsp;~┻━┻",
"q1": 'använd ett lösenord eller <a href="' + SR + '/?h">åk hem</a>',
"r1": "åk hem",
".s1": "skanna om",
"t1": "åtgärd",
"u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder",
"v1": "koppla upp",
"v2": "använd denna server som en lokal disk",
"w1": "byt till https",
"x1": "byt lösenord",
"y1": "redigera utdelningar",
"z1": "lås upp denna utdelning:",
"ta1": "fyll i ditt nya lösenord",
"ta2": "upprepa det nya lösenordet:",
"ta3": "det blev fel; vänligen försök igen",
"aa1": "inkommande filer:",
"ab1": "avaktivera no304",
"ac1": "aktivera no304",
"ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!",
"ae1": "aktiva nedladdningar:",
"af1": "visa senaste uppladdningar",
"ag1": "visa idp-cache"
},
"ukr": {
"a1": "оновити",
"b1": "привітик, незнайомцю &nbsp; <small>(ви не авторизовані)</small>",
"c1": "вийти",
"d1": "трасування стека",
"d2": "показує стан усіх активних потоків",
"e1": "перезавантажити конфіг",
"e2": "перезавантажити файли конфігурації (облікові записи/томи/прапорці),$Nта пересканувати всі томи e2ds$N$Nувага: будь-які зміни глобальних налаштувань$Nвимагають повного перезапуску",
"f1": "ви можете бачити:",
"g1": "ви можете завантажувати файли в:",
"cc1": "всяка всячина:",
"h1": "вимкнути k304",
"i1": "увімкнути k304",
"j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), <em>але</em> це також зробить усе повільнішим загалом",
"k1": "скинути налаштування клієнта",
"l1": "авторизуйтесь для інших опцій:",
"ls3": "увійти", //m
"lu4": "ім'я користувача", //m
"lp4": "пароль", //m
"lo3": "вийти з облікового запису “{0}” всюди", //m
"lo2": "це завершить сеанс у всіх браузерах", //m
"m1": "з поверненням,",
"n1": "404 не знайдено &nbsp;┐( ´ -`)┌",
"o1": 'або у вас немає доступу -- спробуйте авторизуватися або <a href="' + SR + '/?h">повернутися на головну</a>',
"p1": "403 доступ заборонений &nbsp;~┻━┻",
"q1": 'авторизуйтесь або <a href="' + SR + '/?h">поверніться на головну</a>',
"r1": "повернутися на головну",
".s1": "пересканувати",
"t1": "дія",
"u2": "час з останнього запису сервера$N( завантаження / перейменування / ... )$N$N17d = 17 днів$N1h23 = 1 година 23 хвилини$N4m56 = 4 хвилини 56 секунд",
"v1": "підключити",
"v2": "використовувати цей сервер як локальний HDD",
"w1": "перейти на https",
"x1": "змінити пароль",
"y1": "керування доступом",
"z1": "розблокувати:",
"ta1": "спочатку заповніть ваш новий пароль",
"ta2": "повторіть для підтвердження нового пароля:",
"ta3": "описка; спробуйте знову",
"aa1": "вхідні файли:",
"ab1": "вимкнути no304",
"ac1": "увімкнути no304",
"ad1": "увімкнення no304 вимкне все кешування; спробуйте це, якщо k304 було недостатньо. Це витратить величезну кількість мережевого трафіку!",
"ae1": "активні завантаження:",
"af1": "показати нещодавні завантаження",
"ag1": "показати відомих IdP-користувачів"
},
"rus": {
"a1": "обновить",
"b1": "приветик, незнакомец &nbsp; <small>(вы не авторизованы)</small>",
"c1": "выйти",
"d1": "трассировка стека",
"d2": "показывает состояние всех активных потоков",
"e1": "перезагрузить конфиг",
"e2": "перезагрузить файлы конфига (аккаунты/хранилища/флаги),$Nи пересканировать все хранилища с флагом e2ds$N$Nвнимание: изменения глобальных настроек$Nтребуют полного перезапуска сервера",
"f1": "вы можете видеть:",
"g1": "вы можете загружать файлы в:",
"cc1": "всякая всячина:",
"h1": "отключить k304",
"i1": "включить k304",
"j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), <em>но</em> это также сделает работу клиента медленнее",
"k1": "сбросить локальные настройки",
"l1": "авторизуйтесь для других опций:",
"ls3": "войти", //m
"lu4": "имя пользователя", //m
"lp4": "пароль", //m
"lo3": "выйти из “{0}” везде", //m
"lo2": "это завершит сеанс во всех браузерах", //m
"m1": "с возвращением,",
"n1": "404 не найдено &nbsp;┐( ´ -`)┌",
"o1": 'или у вас нет доступа -- попробуйте авторизоваться или <a href="' + SR + '/?h">вернуться на главную</a>',
"p1": "403 доступ запрещён &nbsp;~┻━┻",
"q1": 'авторизуйтесь или <a href="' + SR + '/?h">вернитесь на главную</a>',
"r1": "вернуться на главную",
".s1": "пересканировать",
"t1": "действия",
"u2": "время с последней записи на сервер$N( загрузка / переименование / ... )$N$N17d = 17 дней$N1h23 = 1 час 23 минут$N4m56 = 4 минут 56 секунд",
"v1": "подключить",
"v2": "использовать сервер как локальный диск",
"w1": "перейти на https",
"x1": "поменять пароль",
"y1": "управление доступом",
"z1": "разблокировать:",
"ta1": "сначала введите свой новый пароль",
"ta2": "повторите новый пароль:",
"ta3": "опечатка; попробуйте снова",
"aa1": "входящие файлы:",
"ab1": "отключить no304",
"ac1": "включить no304",
"ad1": "включённый no304 полностью отключит хеширование; используйте, если k304 не помог. Сильно увеличит объём трафика!",
"ae1": "активные скачивания:",
"af1": "показать недавние загрузки",
"ag1": "показать известных IdP-пользователей",
},
}; };
if (window.langmod) if (window.langmod)
@ -111,7 +777,14 @@ for (var k in (d || {})) {
o[a].innerHTML = d[k]; o[a].innerHTML = d[k];
else if (f == 2) else if (f == 2)
o[a].setAttribute("tt", d[k]); o[a].setAttribute("tt", d[k]);
else if (f == 3)
o[a].setAttribute("value", d[k]);
else if (f == 4)
o[a].setAttribute("placeholder", " " + d[k]);
} }
var o1 = ebi('lo'), o2 = ebi('un');
if (o1 && o2 && d.lo3)
o1.setAttribute("value", d.lo3.format(o2.textContent));
try { try {
if (is_idp) { if (is_idp) {
@ -123,8 +796,8 @@ try {
catch (ex) { } catch (ex) { }
tt.init(); tt.init();
var o = QS('input[name="cppwd"]'); var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus(); o.focus();
o = ebi('u'); o = ebi('u');
@ -133,6 +806,9 @@ if (o && /[0-9]+$/.exec(o.innerHTML))
ebi('uhash').value = '' + location.hash; ebi('uhash').value = '' + location.hash;
if (/\&re=/.test('' + location))
ebi('a').className = 'af g';
(function() { (function() {
if (!ebi('x')) if (!ebi('x'))
return; return;

View file

@ -36,12 +36,13 @@
<span class="os lin mac"> <span class="os lin mac">
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint {% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</span> </span>
<a href="#" id="setpw">use real password</a> {% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
<a href="#" id="qr">show qr</a>
</p> </p>
{% if args.idp_h_usr %} {% if args.have_idp_hdrs %}
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p> <p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
{% endif %} {% endif %}
@ -240,14 +241,26 @@
<div class="os win"> <div class="os win">
<h1>ShareX</h1> <h1>ShareX</h1>
<p>to upload screenshots using ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> or <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p> <p>to upload screenshots using ShareX <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p>
<pre class="dl" name="copyparty.sxcu">
{ "Version": "15.0.0", "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"Body": "MultipartFormData", "URL": "{response}",
"RequestMethod": "POST", "FileFormName": "f" }
</pre>
<p>for ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> specifically, save this as <code>copyparty.sxcu</code> and run it:</p>
<pre class="dl" name="copyparty.sxcu"> <pre class="dl" name="copyparty.sxcu">
{ "Name": "copyparty", { "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}", "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": { "Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %} {% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
"accept": "url"
}, },
"DestinationType": "ImageUploader, TextUploader, FileUploader", "DestinationType": "ImageUploader, TextUploader, FileUploader",
"FileFormName": "f" } "FileFormName": "f" }

View file

@ -49,15 +49,17 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk'); setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
ebi('setpw').onclick = function (e) { var pw = '';
function setpw(e) {
ev(e); ev(e);
modal.prompt('password:', '', function (v) { modal.prompt('password:', '', function (v) {
if (!v) if (!v)
return; return;
pw = v;
var pw0 = ebi('pw0').innerHTML, var pw0 = ebi('pw0').innerHTML,
oa = QSA('b'); oa = QSA('b');
for (var a = 0; a < oa.length; a++) for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == pw0) if (oa[a].innerHTML == pw0)
oa[a].textContent = v; oa[a].textContent = v;
@ -65,3 +67,14 @@ ebi('setpw').onclick = function (e) {
add_dls(); add_dls();
}); });
} }
if (ebi('setpw'))
ebi('setpw').onclick = setpw;
ebi('qr').onclick = function () {
var url = ('' + location).split('?')[0];
if (pw)
url += '?pw=' + pw;
var txt = esc(url) + '<img class="b64" width="100" height="100" src="' + addq(url, 'qr') + '" />';
modal.alert(txt);
};

View file

@ -964,6 +964,7 @@ function up2k_init(subtle) {
"t": 0 "t": 0
}, },
"car": 0, "car": 0,
"nre": 0,
"slow_io": null, "slow_io": null,
"oserr": false, "oserr": false,
"modn": 0, "modn": 0,
@ -1572,7 +1573,7 @@ function up2k_init(subtle) {
function linklist() { function linklist() {
var ret = [], var ret = [],
base = document.location.origin.replace(/\/$/, ''); base = location.origin.replace(/\/$/, '');
for (var a = 0; a < st.files.length; a++) { for (var a = 0; a < st.files.length; a++) {
var t = st.files[a], var t = st.files[a],
@ -1595,7 +1596,7 @@ function up2k_init(subtle) {
ev(e); ev(e);
var txt = linklist(); var txt = linklist();
cliptxt(txt + '\n', function () { cliptxt(txt + '\n', function () {
toast.inf(5, un_clip.format(txt.split('\n').length)); toast.inf(5, L.un_clip.format(txt.split('\n').length));
}); });
}; };
@ -1783,8 +1784,7 @@ function up2k_init(subtle) {
} }
var tasker = (function () { var tasker = (function () {
var running = false, var running = false;
was_busy = false;
var defer = function () { var defer = function () {
running = false; running = false;
@ -1801,7 +1801,17 @@ function up2k_init(subtle) {
while (true) { while (true) {
var now = Date.now(), var now = Date.now(),
blocktime = now - r.tact, blocktime = now - r.tact,
is_busy = st.car < st.files.length; was_busy = st.is_busy,
is_busy = !!( // gzip take the wheel
st.car < st.files.length ||
st.busy.hash.length ||
st.todo.hash.length ||
st.busy.handshake.length ||
st.todo.handshake.length ||
st.busy.upload.length ||
st.todo.upload.length ||
st.busy.head.length ||
st.todo.head.length);
if (blocktime > 2500) if (blocktime > 2500)
console.log('main thread blocked for ' + blocktime); console.log('main thread blocked for ' + blocktime);
@ -1809,7 +1819,16 @@ function up2k_init(subtle) {
r.tact = now; r.tact = now;
if (was_busy && !is_busy) { if (was_busy && !is_busy) {
for (var a = 0; a < st.files.length; a++) { var nre = 0, nf = 0;
for (var a = 0; a < st.files.length; a++)
if (st.files[a].want_recheck)
nre++;
console.log('nre', nre, 'st', st.nre);
if (st.nre != nre) {
st.nre = nre;
nf = st.files.length;
}
for (var a = 0; a < nf; a++) {
var t = st.files[a]; var t = st.files[a];
if (t.want_recheck) { if (t.want_recheck) {
t.rechecks++; t.rechecks++;
@ -1817,7 +1836,7 @@ function up2k_init(subtle) {
push_t(st.todo.handshake, t); push_t(st.todo.handshake, t);
} }
} }
is_busy = st.todo.handshake.length; is_busy = !!st.todo.handshake.length;
try { try {
if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused)) if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused))
treectl.goto(); treectl.goto();
@ -1826,7 +1845,7 @@ function up2k_init(subtle) {
} }
if (was_busy != is_busy) { if (was_busy != is_busy) {
st.is_busy = was_busy = is_busy; st.is_busy = is_busy;
window[(is_busy ? "add" : "remove") + window[(is_busy ? "add" : "remove") +
"EventListener"]("beforeunload", warn_uploader_busy); "EventListener"]("beforeunload", warn_uploader_busy);
@ -1947,7 +1966,7 @@ function up2k_init(subtle) {
for (var a = 0; a < st.files.length; a++) { for (var a = 0; a < st.files.length; a++) {
var t = st.files[a]; var t = st.files[a];
if (t.want_recheck && !t.rechecks) if (t.want_recheck && t.rechecks < 999)
return; return;
} }
@ -2511,8 +2530,8 @@ function up2k_init(subtle) {
var msg = []; var msg = [];
for (var a = 0, aa = Math.min(20, response.hits.length); a < aa; a++) { for (var a = 0, aa = Math.min(20, response.hits.length); a < aa; a++) {
var hit = response.hits[a], var hit = response.hits[a],
tr = unix2iso(hit.ts), tr = unix2ui(hit.ts),
tu = unix2iso(t.lmod), tu = unix2ui(t.lmod),
diff = parseInt(t.lmod) - parseInt(hit.ts), diff = parseInt(t.lmod) - parseInt(hit.ts),
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b', cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff; sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
@ -2693,8 +2712,9 @@ function up2k_init(subtle) {
if (ofs !== -1) { if (ofs !== -1) {
err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / '); err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / ');
} }
if (!t.rechecks && (err_pend || err_srcb)) { if (!t.rechecks)
t.rechecks = 0; t.rechecks = 0;
if (t.rechecks < 999 && (err_pend || err_srcb)) {
t.want_recheck = true; t.want_recheck = true;
if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) { if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) {
err = L.u_dupdefer; err = L.u_dupdefer;
@ -2811,7 +2831,7 @@ function up2k_init(subtle) {
if (!t.t_uploading) if (!t.t_uploading)
t.t_uploading = Date.now(); t.t_uploading = Date.now();
pvis.seth(t.n, 1, "🚀 send"); pvis.seth(t.n, 1, "🚀 " + L.ul_send);
var chunksize = get_chunksize(t.size), var chunksize = get_chunksize(t.size),
car = pcar * chunksize, car = pcar * chunksize,
@ -3187,7 +3207,7 @@ function up2k_init(subtle) {
return; return;
try { try {
ebi('lifew').innerHTML = unix2iso((st.lifetime || lifetime) + ebi('lifew').innerHTML = unix2ui((st.lifetime || lifetime) +
Date.now() / 1000 - new Date().getTimezoneOffset() * 60 Date.now() / 1000 - new Date().getTimezoneOffset() * 60
).replace(' ', ', ').slice(0, -3); ).replace(' ', ', ').slice(0, -3);
} }

View file

@ -120,7 +120,7 @@ function esc(txt) {
function basenames(txt) { function basenames(txt) {
return (txt + '').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js'); return (txt + '').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js');
} }
if ((document.location + '').indexOf(',rej,') + 1) if ((location + '').indexOf(',rej,') + 1)
window.onunhandledrejection = function (e) { window.onunhandledrejection = function (e) {
var err = e.reason; var err = e.reason;
try { try {
@ -180,6 +180,9 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
if (!/\.js($|\?)/.exec(url)) if (!/\.js($|\?)/.exec(url))
return; // chrome debugger return; // chrome debugger
if (url.indexOf('extension://') + 1)
return;
if (url.indexOf(' > eval') + 1 && !evalex_fatal) if (url.indexOf(' > eval') + 1 && !evalex_fatal)
return; // md timer return; // md timer
@ -383,8 +386,10 @@ if (!String.prototype.format)
}); });
}; };
var have_URL = false;
try { try {
new URL('/a/', 'https://a.com/'); new URL('/a/', 'https://a.com/');
have_URL = true;
} }
catch (ex) { catch (ex) {
console.log('ie11 shim URL()'); console.log('ie11 shim URL()');
@ -732,6 +737,16 @@ function makeSortable(table, cb) {
} }
function assert_vp(path) {
if (path.indexOf('//') + 1)
throw 'nonlocal1: ' + path;
var o = location.origin;
if (have_URL && (new URL(path, o)).origin != o)
throw 'nonlocal2: ' + path;
}
function linksplit(rp, id) { function linksplit(rp, id) {
var ret = [], var ret = [],
apath = '/', apath = '/',
@ -878,7 +893,7 @@ function uricom_adec(arr, li) {
function get_evpath() { function get_evpath() {
var ret = document.location.pathname; var ret = location.pathname;
if (ret.indexOf('/') !== 0) if (ret.indexOf('/') !== 0)
ret = '/' + ret; ret = '/' + ret;
@ -895,11 +910,29 @@ function noq_href(el) {
} }
function pad2(v) {
return ('0' + v).slice(-2);
}
function unix2iso(ts) { function unix2iso(ts) {
return new Date(ts * 1000).toISOString().replace("T", " ").slice(0, -5); return new Date(ts * 1000).toISOString().replace("T", " ").slice(0, -5);
} }
function unix2iso_localtime(ts) {
var o = new Date(ts * 1000),
p = pad2;
return "{0}-{1}-{2} {3}:{4}:{5}".format(
o.getFullYear(),
p(o.getMonth() + 1),
p(o.getDate()),
p(o.getHours()),
p(o.getMinutes()),
p(o.getSeconds()));
}
function s2ms(s) { function s2ms(s) {
s = Math.floor(s); s = Math.floor(s);
var m = Math.floor(s / 60); var m = Math.floor(s / 60);
@ -987,9 +1020,13 @@ function lhumantime(v) {
if (!L || tp.length < 2 || tp[1].indexOf('$') + 1) if (!L || tp.length < 2 || tp[1].indexOf('$') + 1)
return t; return t;
var ret = ''; var u, n, ret = '';
for (var a = 0; a < tp.length; a += 2) for (var a = 0; a < tp.length; a += 2) {
ret += tp[a] + ' ' + L['ht_' + tp[a + 1] + (tp[a]==1?1:2)] + L.ht_and; n = tp[a];
u = L.ht_h5 ? (n==1 ? 1 : (n>1&&n<5) ? 2 : 5) :
(n==1 ? 1 : 2);
ret += tp[a] + ' ' + L['ht_' + tp[a + 1] + u] + L.ht_and;
}
return ret.slice(0, -L.ht_and.length); return ret.slice(0, -L.ht_and.length);
} }
@ -1191,6 +1228,13 @@ function scfg_bind(obj, oname, cname, defval, cb) {
} }
window.unix2ui = (function () {
var v = sread('utctid');
v = v ? (v === '0') : (window.dutc === false);
return v ? unix2iso_localtime : unix2iso;
})();
function hist_push(url) { function hist_push(url) {
console.log("h-push " + url); console.log("h-push " + url);
try { try {
@ -1209,20 +1253,23 @@ function hist_replace(url) {
function sethash(hv) { function sethash(hv) {
if (window.history && history.replaceState) { if (window.history && history.replaceState) {
hist_replace(document.location.pathname + document.location.search + '#' + hv); hist_replace(location.pathname + location.search + '#' + hv);
} }
else { else {
document.location.hash = hv; location.hash = hv;
} }
} }
function dl_file(url) { function dl_file(url) {
console.log('DL [%s]', url); console.log('DL [%s]', url);
var o = mknod('a'); qsr('#dlfth');
var o = mknod('a', 'dlfth');
o.setAttribute('href', url); o.setAttribute('href', url);
o.setAttribute('download', ''); o.setAttribute('download', '');
o.click(); document.body.appendChild(o);
ebi('dlfth').click();
qsr('#dlfth');
} }

View file

@ -1,3 +1,283 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0810-1226 `v1.19.1` archlinux fix
## 🧪 new features
* new translations:
* #486 French (thx @Tr3yWay996, @Packingdustry, @Alee14, @jakubiakfr, @Equinoxs!) e9ddfccf 7aa21483 b87f8f1b
* #463 Polish (thx @pufereq and @daimond113!) 392a4db5
* #537 Nynorsk (thx @chinatsu!) 3931bc27
* #549 custom mdns domain 3c78c6a8
## 🩹 bugfixes
* #539 FTP glitches when running on windows 8ba98877
* #555 global-config didn't load through PRTY_CONFIG (thx @icxes!) 074e106e
* macos: could take a while to establish webdav connection from finder a01870b7
* ux:
* dropdown colors 347cf6a5
* case-sensitivity in filters e5e82295
* iOS being too enthusiastic about using saved passwords 03acd65e
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0807-2213 `v1.19.0` usernames
## 🧪 new features
* #511 login with username and password (not just password) can now optionally be enabled with `--usernames` 346515cc
* if you have enabled password hashing (`ah-alg: argon2` or similar) then you will need to hash your passwords again after enabling usernames, hashing them as `username:password:`
* #468 add Greek translation (thx @chamdim!) 50f46187 392abd06
* #471 add Czech translation (thx @kubakubakuba!) c9556583
* #515 support systemd socket acivation (thx @mati1210!) 9b9d2a92
* #523 add QR-code to the connectpage bcc3b156
* #513 optional EOL-conversion for texteditor 8b31ed88
* controlpanel refresh-button now toggles automatic refresh 7ae84dea
## 🩹 bugfixes
* fix stuck uploads when the up2k database (`e2d`) is not enabled 4a043568
* if more than 60'000 files were uploaded and there were several dupes of some files, they could get stuck and never upload
* upload performance is improved remarkably by enabling `e2d` so such huge uploads non-e2d had not been tested in a long time
* #467 #470 fix ui-crash when exporting links of all uploaded files to clipboard (thx @geekalaa!) 0df1901f
* #487 fix ui-crash when the location url-part is `//` 0f55a1ae
* fix viewing `.MD` files (8a0746c6)
## 🔧 other changes
* when a reverse-proxy is detected, force explicit configuration of `--rproxy` to obtain correct client IP 3f8cb7e8
* a bit inconvenient, but helps prevent potentially-dangerous misconfiguration
* the necessary configuration changes are explained in the serverlog (you can't miss it)
* thanks to @person4268 for pointing out that there was room for improvements!
* failed login attempts now only log a sha512 hash of the provided password
* to see login-attempts with incorrect passwords as plaintext like before, `log-badpwd: 1`
* #502 add systemd user services and templated services (thx @icxes!) 34d98e99
* #475 improve helptext for multivalue global-options c2ac57a2
* #475 add [chungus.conf](https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf), massive extensive nonsensical demo config b664ebb0
* try to detect proxies with incorrect caching behavior 9e980bb5
* recent-uploads now support ie9 a57f7cc2
* languages and themes are now dropdowns a9ee4f24
* copyparty.exe: upgrade python to 3.13.6 a98360f2
* introduce [copyparty-en.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-en.py), english-only edition of copyparty-sfx.py to save space 33497e6b
## 🗿 known issues
* the `copyparty.pyz` in this release is english-only, and does not include the translations -- they got lost in transit while adjusting the buildscripts to make `copyparty-en.py`
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0804-0013 `v1.18.10` idp speedboost
## 🧪 new features
* #426 add Dutch translation (thx @DeStilleGast!) 3798e19a
* #458 add Italian translation (thx @AOTREVAI!) a38e6e65
* #456 transcode to flac/wav (thx @missaustraliana!) b469db3c b2d48c64 0d09fb68
* #439 config-file can be provided through `PRTY_CONFIG` (thx @icxes!) 971360e9
* #459 videos can become folder thumbnails 16bbcce5
* add `--idp-cookie`, session-tickets for IdP auth (performance boost) f9502c3d
* useful when the IdP-server becomes a bottleneck
## 🩹 bugfixes
* #412 fix PUT-uploads into volumes with `nosub` volflag 47fa4a92
* #435 ignore spurious exceptions from browser extensions 39e55824
* #449 IPv6 QR-Code didn't include port 66a5bf36
* #295 do not force `d2d` in blank vfs (introduced in v1.18.3) 848315c0
## 🔧 other changes
* #440 improved finnish translation (thx @icxes!) a68d5b03
* point to the `-nc` option in the "at max connections" warning 153d240d
* the play-button now indicates "play-as-audio" for video-files 40d56bb3
* docs:
* #411 improve password-hashing instructions (thx @chinponya!) c69c7c8a
* #429 improve `--cert` helptext (thx @kzshantonu!) 7e3825f8
* #413 copyparty is Wii Internet Channel compatible! (thx @techflashYT!) 50f16293
* #461 how to use groups without IdP e85a7107
* mention that WebDAV and OpenGraph are incompatible by default (and how to fix that) 0bc1b8f7
* #345 short explanation about the sfx in quickstart ae5eefc5
* #398 pypi-package now has extra-group `all` 6eaf8af1
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0801-2056 `v1.18.9` fix Denial-of-Service
## ⚠️ ATTN: this release fixes a Denial-of-Service vuln
[CVE-2025-54796](https://github.com/9001/copyparty/security/advisories/GHSA-5662-2rj7-f2v6): an unauthenticated user could make the server grind to a halt by accessing a particular URL
## recent important news
* [v1.18.9 (2025-08-01)](https://github.com/9001/copyparty/releases/tag/v1.18.9) fixed [CVE-2025-54796](https://github.com/9001/copyparty/security/advisories/GHSA-5662-2rj7-f2v6) (Denial-of-Service)
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## 🧪 new features
* #310 translated to Spanish (thx @herruzo99!) a1dfd0be
* #350 translated to Ukrainian (thx @MrMebelMan!) fea45e45
* #321 translated to Russian (thx @A1Asriel!) 0b05c726
* #381 translated to Finnish (thx @icxes and @Permik!) 7ecedb2c
* haha it says surf
* #312 add option to use localtime in the UI ad23b253
* #386 initial packaging for debian (thx @Beethoven-n!) 3c6f0b17
## 🩹 bugfixes
* CVE-2025-54796 / GHSA-5662-2rj7-f2v6 09910ba8
* #347 fix upload-abort when uploading to a share 6d6d79fc
* fix xiu backlog dropping on restart 3222ba3a
* #375 fix crash on really old versions of python2.7 (thx @bb!) b69d5901
* #388 another python2.7 fix: improve unicode support in u2c (thx @KevinXuxuxu!) 9c197535
* log creator of new/blank markdown docs d0d2f206
* #400 config didn't support indenting with tabs c1604288
## 🔧 other changes
* `ack` was changed to `continue` 4fa7be2a
## 🌠 fun facts
* the translations have made the sfx size balloon from 766 to 845 KiB in under a week... nice! keep em coming :tada:
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0731-0833 `v1.18.8` sfx hotfix
## 🩹 bugfixes
* #354 fix `copyparty-sfx.py` failing to start on certain versions of python c17ce4892ecdb4e11437bc2785d132bd8100eaec
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0730-2131 `v1.18.7` SECURITY: fix another XSS
## ⚠️ ATTN: this release fixes an XSS vulnerability
[GHSA-8mx2-rjh8-q3jq](https://github.com/9001/copyparty/security/advisories/GHSA-8mx2-rjh8-q3jq), could let an attacker execute arbitrary JS by tricking you into clicking a malicious URL
Soon there won't be many of these left, surely. Huge thanks to @Ju0x for finding and reporting this.
## 🧪 new features
* #265 uid/gid for new files can be configured per-volume f1959988
* has preconditions; [see readme](https://github.com/9001/copyparty#chmod-and-chown)
* #212 add German translation (thx @rGunti, @Scotsguy, @chocolateimage) 9d32564c
## 🩹 bugfixes
* GHSA-8mx2-rjh8-q3jq a8705e61
* #276 windows: fix segfault (thx @kernel1994 for debugging!) a9d07c63
* #272 webdav: send disk-size and disk-free to clients 4988a55e
* #285 use disk-free sans root-reserve on linux (thx @Arklaum!) c3cc2dde
* cors-check was funky on IPv6 e9684d40
* #325 upgrade sharex example for newer versions 6016ec93
* #300 restore support for old versions of python 2.7 b7ca6f4a
## 🔧 other changes
* shares: the config POST-target is now always the webroot (for ease of IdP configuration) fb7cbc42
* unlist: now applies to the navpane too fbf17be2
* windows: show disk-usage as well, not just disk-free 5c6341e9
* #228 nix-pkg improvements (thx @dtomvan!) 4915b14b
* docker-compose: ensure logs appear in realtime 3cde1f3b
* mention that IdP-volumes and users [can now be persisted](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#but-you-can-enable-idp-volume-persistence) 6069bc9b
* #316 explain a scary-looking thing in the code 053de619
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0728-2320 `v1.18.6` reflink-dedup
## 🧪 new features
* #201 add support for reflink-based dedup on cow filesystems df9feabc
* combine `--dedup` with `--reflink` to enable, or volflags with same name
* a better and safer alternative to the other dedup approaches (symlink/hardlink), but only possible to use in some cases:
* needs linux 5.3 or newer, python 3.14 or newer, btrfs/xfs/zfs
* not available in the docker images yet; needs a new version of python, so maybe next alpine release (november/december 2025)
* ratelimit password changes to impede bruteforcing a2601fd6
* limit is set by `--ban-pwc` (default is 5 changes in 60min)
## 🩹 bugfixes
* #240 nixos: fix unixgroups issue (thx @chinponya!) 7c9c962b
* #246 cbz: use correct page for thumbnail (thx @Scotsguy!) 542a1de1
## 🔧 other changes
* volflag `nosub` now also prevents mkdir 0f2c6235
* improve documentation:
* #229 use the same example UDS path everywhere cb019afe
* [example nginx config](https://github.com/9001/copyparty/blob/hovudstraum/contrib/nginx/copyparty.conf) had misleading cloudflare comment (thx @jmi2k!) 674fc1fe
* more readable `--help-chmod` 03d23dae
* #244 fix typo in `--help` 4f013f64
* #242 hide "use real pw" on connectpage if no accounts (thx @toast003!) 025942a7
* #211 docker: remove deprecated attribute (thx @ptweezy!) 5b98e104
* #190 add the feature-showcase video to the readme (thx @RustoMCSpit!) 43e6da34
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0727-2305 `v1.18.5` SECURITY: fix XSS in media tags
## ⚠️ ATTN: this release fixes an XSS vulnerability
[GHSA-9q4r-x2hj-jmvr](https://github.com/9001/copyparty/security/advisories/GHSA-9q4r-x2hj-jmvr), exploitable in two different ways, could let an attacker execute arbitrary javascript on other users:
* either: tricking someone into clicking a malicious URL to load and execute javascript
* or: uploading a malicious audio file to the server, affecting any successive visitors
so, with new and curious eyes on the project, we are starting off with a bang. Huge thanks to @altperfect for finding and reporting this earlier today.
## recent important news
* [v1.18.5 (2025-07-28)](https://github.com/9001/copyparty/releases/tag/v1.18.5) fixed XSS in display of media tags
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## 🧪 new features
* #214 option to stop playback after one song, and/or at end of folder 6bb27e60
## 🩹 bugfixes
* GHSA-9q4r-x2hj-jmvr 895880ae
* block external m3u files 2228f81f
* #202 the connect-page could show IP-address when it should have used hostnames/domains b0dec83a
* scrolling locked after tailing a file and closing it creatively d197e754
## 🔧 other changes
* #189 the `SameSite` cookie parameter now defaults to `Strict`, increasing CSRF protection ca6d0b8d
* new option `--cookie-lax` reverts to previous value `Lax`
* docker: add FTPS support b4199847
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0725-1841 `v1.18.4` Landmarks
## 🧪 new features
* #182 [Landmarks](https://github.com/9001/copyparty#database-location) edba7fff
* detects that a storage backend is glitching out and disengage the up2k-database as a precaution
* #183 quickdelete 21a96bcf
* new togglebutton `qdel` in the UI which reduces the number of deletion confirmations by one
* global-option `--qdel=0` which can bring it all the way to zero (good luck)
## 🩹 bugfixes
* fix unpost in recently created shares 2d322dd4
* fix filekeys on windows df6d4df4
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0721-2307 `v1.18.3` drop the umask # 2025-0721-2307 `v1.18.3` drop the umask

2141
docs/chungus.conf Normal file

File diff suppressed because it is too large Load diff

View file

@ -330,7 +330,6 @@ the features you can opt to drop are
* `cm`/easymde, the "fancy" markdown editor, saves ~89k * `cm`/easymde, the "fancy" markdown editor, saves ~89k
* `hl`, prism, the syntax hilighter, saves ~41k * `hl`, prism, the syntax hilighter, saves ~41k
* `fnt`, source-code-pro, the monospace font, saves ~9k * `fnt`, source-code-pro, the monospace font, saves ~9k
* `dd`, the custom mouse cursor for the media player tray tab, saves ~2k
for the `re`pack to work, first run one of the sfx'es once to unpack it for the `re`pack to work, first run one of the sfx'es once to unpack it
@ -355,7 +354,7 @@ pip install mutagen # audio metadata
pip install pyftpdlib # ftp server pip install pyftpdlib # ftp server
pip install partftpy # tftp server pip install partftpy # tftp server
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
pip install Pillow pyheif-pillow-opener # thumbnails pip install Pillow pillow-heif # thumbnails
pip install pyvips # faster thumbnails pip install pyvips # faster thumbnails
pip install psutil # better cleanup of stuck metadata parsers on windows pip install psutil # better cleanup of stuck metadata parsers on windows
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
@ -415,6 +414,8 @@ to get started, first `cd` into the `scripts` folder
* if you want to build the `.pyz` standalone "binary", now run `./make-pyz.sh` * if you want to build the `.pyz` standalone "binary", now run `./make-pyz.sh`
* if you want to build the `tar.gz` for use in a linux-distro package, now run `./make-tgz-release.sh theVersionNumber`
* if you want to build a pypi package, now run `./make-pypi-release.sh d` * if you want to build a pypi package, now run `./make-pypi-release.sh d`
* if you want to build a docker-image, you have two options: * if you want to build a docker-image, you have two options:

View file

@ -6,7 +6,7 @@
[global] [global]
e2dsa # enable file indexing and filesystem scanning e2dsa # enable file indexing and filesystem scanning
e2ts # enable multimedia indexing e2ts # enable multimedia indexing
ansi # enable colors in log messages 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 # q, lo: /cfg/log/%Y-%m%d.log # log to file instead of docker

View file

@ -1,4 +1,3 @@
version: '3'
services: services:
copyparty: copyparty:
@ -11,9 +10,12 @@ services:
- ./:/cfg:z - ./:/cfg:z
- /path/to/your/fileshare/top/folder:/w:z - /path/to/your/fileshare/top/folder:/w:z
# enabling mimalloc by replacing "NOPE" with "2" will make some stuff twice as fast, but everything will use twice as much ram:
environment: environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
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 stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
healthcheck: healthcheck:

View file

@ -27,6 +27,9 @@ services:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
authelia: authelia:
image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax
container_name: idp_authelia container_name: idp_authelia

View file

@ -27,6 +27,9 @@ services:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
traefik: traefik:
image: traefik:v2.11 image: traefik:v2.11
container_name: traefik container_name: traefik

View file

@ -9,9 +9,9 @@ in the copyparty `[global]` config, specify which headers to read client info fr
# important notes # important notes
## IdP volumes are forgotten on shutdown ## by default, IdP volumes are forgotten on shutdown
IdP volumes, meaning dynamically-created volumes, meaning volumes that contain `${u}` or `${g}` in their URL, will be forgotten during a server restart and then "revived" when the volume's owner sends their first request after the restart IdP volumes, meaning dynamically-created volumes, meaning volumes that contain `${u}` or `${g}` in their URL, will (by default) be forgotten during a server restart and then "revived" when the volume's owner sends their first request after the restart
until each IdP volume is revived, it will inherit the permissions of its parent volume (if any) until each IdP volume is revived, it will inherit the permissions of its parent volume (if any)
@ -19,7 +19,17 @@ this means that, if an IdP volume is located inside a folder that is readable by
and likewise -- if the IdP volume is inside a folder that is only accessible by certain users, but the IdP volume is configured to allow access from unauthenticated users, then the contents of the volume will NOT be accessible until it is revived and likewise -- if the IdP volume is inside a folder that is only accessible by certain users, but the IdP volume is configured to allow access from unauthenticated users, then the contents of the volume will NOT be accessible until it is revived
until this limitation is fixed (if ever), it is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the "strategic volumes" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf) it is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the "strategic volumes" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf)
## but you can enable IdP volume persistence
global-option `idp-store` can enable user/group persistence across restarts;
* `idp-store: 1` (default) will log users into a database, but not actually "remember" them (the knowledge is ignored)
* `idp-store: 2` remembers usernames only
* `idp-store: 3` remembers usernames and their groups
the reason why this is default-disabled, is because you may expect copyparty to forget about a user when you delete them from the IdP-server; this will not be the case any longer, you will need to click `view idp cache` in the controlpanel and manually remove the users you want gone
## Connecting webdav clients ## Connecting webdav clients

216
docs/logo-sq.svg Normal file
View file

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="205mm"
height="205mm"
viewBox="0 0 205 205"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="logo-sq.svg"
inkscape:export-filename="logo-sq.png"
inkscape:export-xdpi="126.9"
inkscape:export-ydpi="126.9"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.7165"
inkscape:cx="367.1"
inkscape:cy="434.1"
inkscape:window-width="1920"
inkscape:window-height="1022"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer5" />
<title
id="title1">copyparty_logo</title>
<defs
id="defs1">
<linearGradient
inkscape:collect="always"
id="linearGradient1">
<stop
style="stop-color:#ffcc55;stop-opacity:1"
offset="0"
id="stop1" />
<stop
style="stop-color:#ffcc00;stop-opacity:1"
offset="0.2"
id="stop2" />
<stop
style="stop-color:#ff8800;stop-opacity:1"
offset="1"
id="stop3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="15"
y1="15"
x2="15"
y2="143"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7149,0,0,1,41.29,0)" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>copyparty_logo</dc:title>
<dc:source>github.com/9001/copyparty</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="kassett"
transform="translate(-46.02)">
<rect
style="fill:#333333;stroke-width:1.00023"
id="rect1"
width="205"
height="205"
x="46.02"
y="0"
rx="12.01"
ry="12" />
<rect
style="fill:url(#linearGradient2);stroke-width:0.999736"
id="rect2"
width="193"
height="128"
x="52.02"
y="15"
rx="7.995"
ry="8" />
<rect
style="fill:#333333;stroke-width:0.999983"
id="rect3"
width="142"
height="52"
x="77.52"
y="77.83"
rx="26"
ry="26" />
<circle
style="fill:#cccccc"
id="circle1"
cx="104.5"
cy="103.6"
r="18" />
<circle
style="fill:#cccccc"
id="circle2"
cx="192.5"
cy="103.6"
r="18" />
<path
style="fill:#737373;stroke-width:0.999995px"
d="m 71.53,205 7.55,-39 c 1.35,-6.2 4.23,-7.8 9.05,-8 45.27,-1 75.47,-1 120.77,0 4.9,0.2 7.5,1.8 9.1,8 l 7.5,39 z"
id="path1"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="tekst"
style="display:none"
transform="translate(-46.02)">
<text
xml:space="preserve"
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
x="47.15"
y="55.55"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
x="47.15"
y="55.55"
style="-inkscape-font-specification:Akbar"
rotate="0 0">copyparty</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="stensatt"
transform="translate(-46.02)">
<path
d="m 119.3,58.82 q -2.2,2.24 -11.8,5.54 -8.83,3.12 -10.89,3.12 -8.1,0 -12.54,-5.04 -4.3,-4.81 -4.3,-12.9 0,-11.06 9.24,-18.36 8.16,-6.5 17.99,-6.5 1.2,0 2.4,1.78 1.3,1.67 1.3,2.78 0,4.96 -5.4,6.5 -3.4,0.95 -10.06,2.78 -5.41,2.81 -5.41,10.58 0,7.71 7.14,7.71 2.1,0 2.1,0 1.43,0 3.53,-0.39 2.8,-0.56 7.2,-1.78 4.4,-1.31 5.3,-1.31 0.8,0 4.3,1.45 z"
style="fill:#333333;stroke-width:0.999757"
id="path11" />
<path
d="m 169.6,42.52 q 0,5.02 -3.8,7.89 -3.5,2.68 -9.3,2.68 -1.4,0 -5.3,-0.71 -3.7,-0.73 -5.1,-0.73 -3.9,0 -3.9,4.74 0,1.53 0.3,4.66 0.4,3.13 0.4,4.66 0,3.4 -1.5,5.07 -2.5,0 -5.5,-1.52 -2.6,-17.03 -2.6,-21.52 0,-6.53 3.2,-14.5 4.4,-10.85 12.1,-10.85 7.3,0 14.5,7.34 6.5,6.89 6.5,12.8 z m -7.5,0.36 Q 160.9,38.94 157,35 q -4.5,-4.49 -8.5,-4.49 -2.7,0 -4.5,3.94 -1.7,3.23 -1.7,5.85 0,0.84 0.7,2.22 0.7,1.43 1.5,1.43 2.2,0 6.4,0.54 4,0.55 6.2,0.55 0.5,0 2.6,-0.73 2,-0.84 2.6,-1.32 z"
style="fill:#333333;stroke-width:0.999656"
id="path13" />
<path
d="m 218.3,43.94 q 0,5.01 -3.6,7.89 -3.3,2.67 -8.6,2.67 -1.4,0 -5.1,-0.7 -3.5,-0.73 -4.9,-0.73 -3.7,0 -3.7,4.74 0,1.51 0.4,4.65 0.4,3.13 0.4,4.67 0,3.4 -1.4,5.07 -2.7,0 -5.6,-1.53 -2.7,-17.02 -2.7,-21.42 0,-6.53 3.4,-14.5 4.3,-10.85 11.9,-10.85 6.8,0 13.3,7.34 6.2,6.89 6.2,12.8 z m -7.2,0.36 q -1.1,-3.94 -4.7,-7.88 -4.3,-4.49 -8,-4.49 -2.5,0 -4.4,3.95 -1.6,3.21 -1.6,5.84 0,0.84 0.8,2.22 0.6,1.43 1.5,1.43 2,0 5.9,0.55 3.8,0.53 5.9,0.53 0.6,0 2.4,-0.72 1.9,-0.84 2.5,-1.33 z"
style="fill:#333333;stroke-width:0.999656"
id="path15" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagger"
transform="translate(-46.02)"
style="display:none">
<g
id="g1"
transform="translate(13.52,4.761)">
<path
id="path4"
style="fill:#333333"
d="m 111.4,83.33 -9.5,5.5 2.5,4.33 9.5,-5.5 z m -33.78,19.47 -9.52,5.5 2.5,4.4 9.52,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path5"
style="fill:#333333"
d="m 88.5,73 v 11 h 5 V 73 Z m 0,39 v 11 h 5 v -11 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path6"
style="fill:#333333"
d="m 68.1,87.67 9.53,5.5 2.5,-4.33 -9.53,-5.5 z m 33.8,19.53 9.5,5.5 2.5,-4.4 -9.5,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
<g
id="g2"
transform="rotate(30,132.7,290.2)">
<path
id="path7"
style="fill:#333333"
d="m 111.4,83.33 -9.5,5.5 2.5,4.33 9.5,-5.5 z m -33.78,19.47 -9.52,5.5 2.5,4.4 9.52,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path8"
style="fill:#333333"
d="m 88.5,73 v 11 h 5 V 73 Z m 0,39 v 11 h 5 v -11 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path9"
style="fill:#333333"
d="m 68.1,87.67 9.53,5.5 2.5,-4.33 -9.53,-5.5 z m 33.8,19.53 9.5,5.5 2.5,-4.4 -9.5,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

209
docs/logo256.svg Normal file
View file

@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="256mm"
height="176mm"
viewBox="0 0 256 176"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title1">copyparty_logo</title>
<defs
id="defs1">
<linearGradient
inkscape:collect="always"
id="linearGradient1">
<stop
style="stop-color:#ffcc55;stop-opacity:1"
offset="0"
id="stop1" />
<stop
style="stop-color:#ffcc00;stop-opacity:1"
offset="0.2"
id="stop2" />
<stop
style="stop-color:#ff8800;stop-opacity:1"
offset="1"
id="stop3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="12"
y1="12"
x2="12"
y2="122"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>copyparty_logo</dc:title>
<dc:source>github.com/9001/copyparty</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="kassett">
<rect
style="fill:#333333"
id="rect1"
width="256"
height="175"
x="0"
y="0"
rx="12"
ry="12" />
<rect
style="fill:url(#linearGradient2)"
id="rect2"
width="232"
height="110"
x="12"
y="12"
rx="8"
ry="8" />
<rect
style="fill:#333333"
id="rect3"
width="148"
height="44"
x="54"
y="62"
rx="22"
ry="22" />
<circle
style="fill:#cccccc"
id="circle1"
cx="77"
cy="84"
r="15" />
<circle
style="fill:#cccccc"
id="circle2"
cx="179"
cy="84"
r="15" />
<path
style="fill:#737373"
d="m 41,176 8.5,-32.7 c 1.5,-5.2 4.8,-6.5 10.2,-6.7 51.2,-0.8 85.3,-0.8 136.5,0 5.5,0.17 8.5,1.5 10.2,6.7 L 215,176 Z"
id="path1"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="tekst"
style="display:none">
<text
xml:space="preserve"
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
x="47.153069"
y="55.548954"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
x="47.153069"
y="55.548954"
style="-inkscape-font-specification:Akbar"
rotate="0 0">copyparty</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="stensatt">
<path
d="m 54.39,43.64 q -0.73,0.79 -4.04,1.96 -3.07,1.11 -3.75,1.11 -2.82,0 -4.36,-1.79 -1.49,-1.71 -1.49,-4.58 0,-3.92 3.21,-6.52 2.81,-2.3 6.23,-2.3 0.34,0 0.79,0.63 0.46,0.6 0.46,0.99 0,1.76 -1.87,2.3 -1.17,0.34 -3.45,0.99 -1.88,0.99 -1.88,3.76 0,2.73 2.48,2.73 0.72,0 0.72,0 0.46,0 1.23,-0.14 0.94,-0.19 2.47,-0.63 1.54,-0.46 1.82,-0.46 0.34,0 1.49,0.51 z"
style="fill:#333333"
id="path11" />
<path
d="m 74.95,38.6 q 0,3.58 -3.16,5.93 -2.73,1.96 -5.86,1.96 -2.9,0 -5.12,-2.21 -2.13,-2.22 -2.13,-5.12 0,-3.08 2.68,-5.67 2.73,-2.56 5.8,-2.56 2.99,0 5.38,2.35 2.41,2.36 2.41,5.34 z m -2.9,0.14 q 0,-1.92 -1.49,-3.16 -1.45,-1.28 -3.42,-1.28 -0.1,0 -1.36,1.37 -1.23,1.32 -2.08,1.32 -0.52,0 -0.69,-0.26 -0.99,1.96 -0.99,2.56 0,1.92 1.82,2.9 1.37,0.77 3.07,0.77 1.71,0 3.21,-0.94 1.92,-1.19 1.92,-3.27 z"
style="fill:#333333"
id="path12" />
<path
d="m 96.46,40.14 q 0,2.39 -1.63,3.75 -1.53,1.28 -4.01,1.28 -0.59,0 -2.3,-0.34 -1.62,-0.34 -2.22,-0.34 -1.79,0 -1.79,2.25 0,0.73 0.2,2.22 0.17,1.49 0.17,2.22 0,1.62 -0.66,2.41 -1.23,0 -2.56,-0.72 -1.25,-8.11 -1.25,-10.24 0,-3.12 1.5,-6.91 2.02,-5.17 5.5,-5.17 3.16,0 6.23,3.5 2.82,3.28 2.82,6.1 z m -3.25,0.17 q -0.51,-1.88 -2.22,-3.76 -1.96,-2.13 -3.66,-2.13 -1.11,0 -1.99,1.88 -0.77,1.53 -0.77,2.78 0,0.4 0.32,1.06 0.37,0.68 0.73,0.68 0.94,0 2.73,0.26 1.79,0.25 2.73,0.25 0.26,0 1.11,-0.34 0.85,-0.4 1.11,-0.63 z"
style="fill:#333333"
id="path13" />
<path
d="m 113.7,34.33 q -1.8,3.5 -2.7,5.98 -0.1,0.25 -1.4,3.84 -0.3,1.16 -0.9,3.58 -0.4,2.42 -0.8,3.59 -0.9,2.41 -2,2.25 -1.2,-0.17 -1.3,-1.37 0,-0.17 0,-0.42 0,-0.14 0.2,-1.28 0.9,-4.3 0.9,-5.5 0,-0.46 -0.1,-0.63 -1.2,-2.08 -3.5,-6.32 -2.32,-4.24 -2.1,-6.57 1.3,-1.16 1.8,-1.16 0.4,0 1,0.52 0.5,0.51 0.6,0.93 0.7,5.29 4.1,9.48 0.9,-1.54 1.6,-3.45 0.4,-1.2 1.4,-3.54 1.6,-3.81 2.9,-3.81 0.1,0 0.3,0.1 0.8,0.25 1.1,2.39 z"
style="fill:#333333"
id="path14" />
<path
d="m 134.6,41.16 q 0,2.39 -1.6,3.76 -1.6,1.28 -4,1.28 -0.6,0 -2.3,-0.35 -1.7,-0.34 -2.3,-0.34 -1.7,0 -1.7,2.26 0,0.72 0.2,2.21 0.2,1.5 0.2,2.22 0,1.62 -0.6,2.42 -1.3,0 -2.6,-0.73 -1.3,-8.1 -1.3,-10.19 0,-3.12 1.6,-6.92 1.9,-5.16 5.4,-5.16 3.2,0 6.2,3.5 2.8,3.28 2.8,6.09 z m -3.2,0.17 q -0.6,-1.88 -2.3,-3.75 -1.9,-2.14 -3.6,-2.14 -1.1,0 -2,1.88 -0.8,1.54 -0.8,2.78 0,0.4 0.4,1.06 0.3,0.68 0.7,0.68 0.9,0 2.7,0.26 1.8,0.26 2.7,0.26 0.3,0 1.1,-0.35 0.9,-0.4 1.1,-0.63 z"
style="fill:#333333"
id="path15" />
<path
d="m 155.5,45.68 q 0,0.77 -0.5,1.28 -0.5,0.52 -1.2,0.52 -1.4,0 -2.6,-0.77 -1.2,-0.8 -1.8,-1.97 -0.5,-0.1 -1.2,0.73 -0.8,0.99 -1,1.06 -1,0.46 -3.3,0.46 -1.9,0 -3.3,-2.08 -1.3,-1.82 -1.3,-3.42 0,-2.9 2.9,-5.46 2.7,-2.47 5.7,-2.47 0.8,0 1.5,0.51 0.6,0.51 0.6,1.23 0,0.46 -0.3,0.94 2.1,0.77 2.1,2.41 0,0.3 -0.1,0.9 -0.1,0.6 -0.1,0.89 0,0.35 0.1,0.52 0.4,1.11 2.1,2.9 1.6,1.62 1.6,1.87 z m -6.9,-8.62 q -0.3,0 -0.9,-0.1 -0.7,-0.14 -1,-0.14 -1.1,0 -2.7,1.66 -1.6,1.65 -1.6,2.81 0,0.69 0.6,1.54 0.7,1.11 1.8,1.11 2.3,0 3,-2.47 0.5,-2.22 0.9,-4.41 z"
style="fill:#333333"
id="path16" />
<path
d="m 174.1,36.38 q -0.3,0.34 -1.3,0.34 -0.7,0 -2.1,-0.25 -1.5,-0.26 -2.1,-0.26 -4,0 -4.7,5.89 -0.3,2.64 -0.4,2.82 -0.3,0.85 -1.4,1.96 h -1 q -0.6,-1.03 -1.1,-3.5 -0.5,-2.36 -0.5,-3.64 0,-0.99 0.1,-1.28 0.2,-0.47 0.9,-0.47 0.2,0 0.5,0.26 0.3,0.26 0.3,0.26 1.6,-3.02 2.7,-3.93 1.5,-1.45 4.3,-1.45 1.2,0 3.1,0.77 2.4,0.99 2.8,2.39 z"
style="fill:#333333"
id="path17" />
<path
d="m 196,31.91 q 0.3,0.68 0.3,1.23 0,1.59 -2.1,1.59 -0.8,0 -2.9,-0.43 -2.2,-0.46 -2.9,-0.46 -1.1,0 -1.3,0.1 -0.4,0.18 -0.4,1.03 0,1.88 0.6,5.89 0.5,5 1.3,5.23 -0.3,0.3 -0.3,0.94 -1,0.59 -2.2,0.59 -1.2,0 -1.8,-3.32 -0.1,-1.17 -0.4,-6.63 -0.1,-3.92 -0.7,-4.69 -0.2,-0.4 -3.6,-0.3 -0.9,0 -1.4,0.1 -0.4,0 -0.3,0 -0.6,0 -1,-0.6 -0.4,-1.11 -0.4,-1.2 0,-1.22 3.5,-1.7 1.4,-0.14 4,-0.43 0,-0.72 -0.1,-2.18 0,-1.5 0,-2.22 0,-3.71 1.8,-3.71 0.4,0 1,0.51 0.5,0.51 0.5,0.94 v 6.74 q 0.9,1.02 4.2,1.45 3.4,0.43 4.6,1.59 z"
style="fill:#333333"
id="path18" />
<path
d="m 214.6,34.5 q -1.7,3.5 -2.8,5.98 -0.1,0.25 -1.3,3.84 -0.4,1.16 -0.8,3.58 -0.4,2.42 -0.9,3.59 -0.8,2.41 -2,2.25 -1.2,-0.17 -1.3,-1.37 -0.1,-0.17 -0.1,-0.42 0,-0.14 0.3,-1.28 0.9,-4.3 0.9,-5.5 0,-0.46 -0.1,-0.63 -1.2,-2.08 -3.5,-6.31 -2.3,-4.25 -2,-6.58 1.2,-1.16 1.8,-1.16 0.3,0 0.8,0.52 0.5,0.51 0.6,0.93 0.8,5.3 4.2,9.48 0.9,-1.54 1.6,-3.45 0.5,-1.2 1.4,-3.54 1.5,-3.81 2.9,-3.81 0.2,0 0.3,0.1 0.7,0.25 1.1,2.39 z"
style="fill:#333333"
id="path19" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagger">
<g
id="g1">
<path
id="path4"
style="fill:#333333"
d="m 94.2,71.8 -8,4.6 2,3.5 8,-4.7 z m -28.4,16.3 -8,4.7 2,3.4 8,-4.6 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path5"
style="fill:#333333"
d="m 75,63 v 9.2 h 4 V 63 Z m 0,32.8 v 9.2 h 4 v -9.2 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path6"
style="fill:#333333"
d="m 96.2,92.8 -8,-4.7 -2,3.5 8,4.6 z m -28.4,-16.4 -8,-4.6 -2,3.4 8,4.7 z"
sodipodi:nodetypes="cccccccccc" />
</g>
<g
id="g2">
<path
id="path7"
style="fill:#333333"
d="m 200,82 h -9 v 4 h 9 z m -33,0 h -9 v 4 h 9 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path8"
style="fill:#333333"
d="m 191,101 -4,-7.8 -4,2 5,7.8 z m -16,-28.2 -5,-8 -3,2 4,8 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path9"
style="fill:#333333"
d="m 170,103 5,-7.8 -4,-2 -4,7.8 z m 17,-28.2 4,-8 -3,-2 -5,8 z"
sodipodi:nodetypes="cccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -161,7 +161,7 @@ symbol legend,
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ | | upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ |
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ | | parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | | | | resumable uploads | █ | | █ | | | | | | █ | █ | █ | | |
| upload segmenting | █ | | | | | | | █ | █ | █ | █ | | █ | | upload segmenting | █ | | | | | | | █ | █ | █ | █ | | █ |
| upload acceleration | █ | | | | | | | | █ | | █ | | | | upload acceleration | █ | | | | | | | | █ | | █ | | |
| upload verification | █ | | | █ | █ | | | | █ | | | | | | upload verification | █ | | | █ | █ | | | | █ | | | | |
| upload deduplication | █ | | | | █ | | | | █ | | | | | | upload deduplication | █ | | | | █ | | | | █ | | | | |
@ -488,7 +488,7 @@ symbol legend,
* ⚠️ [isolated on-disk file hierarchy] in per-user folders * ⚠️ [isolated on-disk file hierarchy] in per-user folders
* not that bad, can probably be remedied with bindmounts or maybe symlinks * not that bad, can probably be remedied with bindmounts or maybe symlinks
* ⚠️ uploads not resumable / accelerated / integrity-checked * ⚠️ uploads not resumable / accelerated / integrity-checked
* ⚠️ on cloudflare: max upload size 100 MiB * 🔵 uploads are segmented; no filesize limit, even on cloudflare
* ⚠️ uploading small files is slow; `4` files per sec (copyparty does `670`/sec, 160x faster) * ⚠️ uploading small files is slow; `4` files per sec (copyparty does `670`/sec, 160x faster)
* ⚠️ no write-only / upload-only folders * ⚠️ no write-only / upload-only folders
* ⚠️ http/webdav only; no ftp, zeroconf * ⚠️ http/webdav only; no ftp, zeroconf

View file

@ -4,16 +4,27 @@
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils }: outputs =
{
self,
nixpkgs,
flake-utils,
}:
{ {
nixosModules.default = ./contrib/nixos/modules/copyparty.nix; nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
overlays.default = self: super: { overlays.default = final: prev: {
copyparty = copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
self.python3.pkgs.callPackage ./contrib/package/nix/copyparty { ffmpeg = final.ffmpeg-full;
ffmpeg = self.ffmpeg-full; };
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./contrib/package/nix/partftpy { };
}; };
};
}; };
} // flake-utils.lib.eachDefaultSystem (system: }
// flake-utils.lib.eachDefaultSystem (
system:
let let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
@ -22,10 +33,29 @@
}; };
overlays = [ self.overlays.default ]; overlays = [ self.overlays.default ];
}; };
in { in
{
# check that copyparty builds with all optionals turned on
checks.copyparty-full = self.packages.${system}.copyparty.override {
withHashedPasswords = true;
withCertgen = true;
withThumbnails = true;
withFastThumbnails = true;
withMediaProcessing = true;
withBasicAudioMetadata = true;
withZeroMQ = true;
withFTPS = true;
withSMB = true;
};
packages = { packages = {
inherit (pkgs) copyparty; inherit (pkgs)
copyparty
;
default = self.packages.${system}.copyparty; default = self.packages.${system}.copyparty;
}; };
});
formatter = pkgs.nixfmt-tree;
}
);
} }

View file

@ -45,6 +45,14 @@ classifiers = [
"Demo Server" = "https://a.ocv.me/pub/demo/" "Demo Server" = "https://a.ocv.me/pub/demo/"
[project.optional-dependencies] [project.optional-dependencies]
all = [
"argon2-cffi",
"partftpy>=0.4.0",
"Pillow",
"pyftpdlib",
"pyopenssl",
"pyzmq",
]
thumbnails = ["Pillow"] thumbnails = ["Pillow"]
thumbnails2 = ["pyvips"] thumbnails2 = ["pyvips"]
audiotags = ["mutagen"] audiotags = ["mutagen"]
@ -86,7 +94,6 @@ copyparty = [
"web/*.css", "web/*.css",
"web/*.html", "web/*.html",
"web/a/*.bat", "web/a/*.bat",
"web/dd/*.png",
"web/deps/*.gz", "web/deps/*.gz",
"web/deps/*.woff*", "web/deps/*.woff*",
] ]

View file

@ -137,10 +137,10 @@ repack() {
} }
repack sfx-full "re gz" repack sfx-full "re gz"
repack sfx-ent "re no-dd" repack sfx-ent "re"
repack sfx-ent "re no-dd gz" repack sfx-ent "re gz"
repack sfx-lite "re no-dd no-cm no-hl" repack sfx-lite "re no-cm no-hl"
repack sfx-lite "re no-dd no-cm no-hl gz" repack sfx-lite "re no-cm no-hl gz"
# move fuse and up2k clients into copyparty-extras/, # move fuse and up2k clients into copyparty-extras/,

View file

@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \ py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
ffmpeg ffmpeg
COPY i/dist/copyparty-sfx.py innvikler.sh ./ COPY i/dist/copyparty-sfx.py innvikler.sh ./

View file

@ -12,20 +12,23 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/ COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \ RUN apk add -U !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \ py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
py3-pip py3-cffi \ py3-pip py3-cffi \
ffmpeg \ ffmpeg \
py3-magic \ py3-magic \
vips-jxl vips-heif vips-poppler vips-magick \ vips-jxl vips-heif vips-poppler vips-magick \
py3-numpy fftw libsndfile \ py3-numpy fftw libsndfile \
vamp-sdk vamp-sdk-libs \ vamp-sdk vamp-sdk-libs \
libraw py3-numpy cython \
&& apk add -t .bd \ && apk add -t .bd \
bash wget gcc g++ make cmake patchelf \ bash wget gcc g++ make cmake patchelf \
python3-dev ffmpeg-dev fftw-dev libsndfile-dev \ python3-dev ffmpeg-dev fftw-dev libsndfile-dev \
py3-wheel py3-numpy-dev libffi-dev \ py3-wheel py3-numpy-dev libffi-dev \
vamp-sdk-dev \ vamp-sdk-dev \
libraw-dev py3-numpy-dev \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \ && python3 -m pip install pyvips \
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
&& bash install-deps.sh \ && bash install-deps.sh \
&& apk del py3-pip .bd \ && apk del py3-pip .bd \
&& chmod 777 /root \ && chmod 777 /root \

View file

@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pillow py3-mutagen py3-jinja2 py3-argon2-cffi py3-openssl py3-pillow py3-mutagen
COPY i/dist/copyparty-sfx.py innvikler.sh ./ COPY i/dist/copyparty-sfx.py innvikler.sh ./
ADD base ./base ADD base ./base

View file

@ -9,16 +9,19 @@ ENV XDG_CONFIG_HOME=/cfg
RUN apk add -U !pyc \ RUN apk add -U !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \ py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
py3-pip py3-cffi \ py3-pip py3-cffi \
ffmpeg \ ffmpeg \
py3-magic \ py3-magic \
vips-jxl vips-heif vips-poppler vips-magick \ vips-jxl vips-heif vips-poppler vips-magick \
libraw py3-numpy cython \
&& apk add -t .bd \ && apk add -t .bd \
bash wget gcc g++ make cmake patchelf \ bash wget gcc g++ make cmake patchelf \
python3-dev py3-wheel libffi-dev \ python3-dev py3-wheel libffi-dev \
libraw-dev py3-numpy-dev \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \ && python3 -m pip install pyvips \
&& python3 -m pip install "$(wget -O- https://api.github.com/repos/letmaik/rawpy/releases/latest | awk -F\" '$2=="tarball_url"{print$4}')" \
&& apk del py3-pip .bd && apk del py3-pip .bd
COPY i/dist/copyparty-sfx.py innvikler.sh ./ COPY i/dist/copyparty-sfx.py innvikler.sh ./

66
scripts/make-rpm.sh Executable file
View file

@ -0,0 +1,66 @@
#!/bin/bash
set -e
#--localbuild to build webdeps and tar locally; otherwise just download prebuilt
#--pm change packagemanager; otherwise default to dnf
while [ ! -z "$1" ]; do
case $1 in
local-build) local_build=1 ; ;;
pm) shift;packagemanager="$1"; ;;
esac
shift
done
[ -e copyparty/__main__.py ] || cd ..
[ -e copyparty/__main__.py ] ||
{
echo "run me from within the project root folder"
echo
exit 1
}
packagemanager=${packagemanager:-dnf}
ver=$(awk '/^VERSION/{gsub(/[^0-9]/," ");printf "%d.%d.%d\n",$1,$2,$3}' copyparty/__version__.py)
releasedir="dist/temp_copyparty_$ver"
sourcepkg="copyparty-$ver.tar.gz"
#make temporary directory to build rpm in
mkdir -p $releasedir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
trap "rm -rf $releasedir" EXIT
# make/get tarball
if [ $local_build ]; then
if [ ! -f "copyparty/web/deps/mini-fa.woff" ]; then
sudo $packagemanager update
sudo $packagemanager install podman-docker docker
make -C deps-docker
fi
if [ ! -f "dist/$sourcepkg" ]; then
./$cppdir/scripts/make-sfx.sh gz fast # pulls some build-deps + good smoketest
./$cppdir/scripts/make-tgz-release.sh "$ver"
fi
else
if [ ! -f "dist/$sourcepkg" ]; then
curl -OL https://github.com/9001/copyparty/releases/download/v$ver/$sourcepkg --output-dir dist
fi
fi
cp dist/$sourcepkg "$releasedir/SOURCES/$sourcepkg"
cp "contrib/package/rpm/copyparty.spec" "$releasedir/SPECS/"
sed -i "s/\$pkgver/$ver/g" "$releasedir/SPECS/copyparty.spec"
sed -i "s/\$pkgrel/1/g" "$releasedir/SPECS/copyparty.spec"
sudo $packagemanager update
sudo $packagemanager install \
rpmdevtools python-devel pyproject-rpm-macros \
python-wheel python-setuptools python-jinja2 \
make pigz
cd "$releasedir/"
rpmbuild --define "_topdir `pwd`" -bb SPECS/copyparty.spec
cd -
rpm="copyparty-$ver-1.noarch.rpm"
mv "$releasedir/RPMS/noarch/$rpm" dist/$rpm

View file

@ -46,8 +46,6 @@ help() { exec cat <<'EOF'
# `no-fnt` saves ~9k by removing the source-code-pro font # `no-fnt` saves ~9k by removing the source-code-pro font
# (browsers will try to use 'Consolas' instead) # (browsers will try to use 'Consolas' instead)
# #
# `no-dd` saves ~2k by removing the mouse cursor
#
# _____________________________________________________________________ # _____________________________________________________________________
# build behavior: # build behavior:
# #
@ -61,8 +59,8 @@ help() { exec cat <<'EOF'
# #
# _____________________________________________________________________ # _____________________________________________________________________
# some usage examples: # some usage examples:
# ./scripts/make-sfx.sh lang eng no-cm no-hl no-dd no-fnt no-smb no-pf # ./scripts/make-sfx.sh lang eng no-cm no-hl no-fnt no-smb no-pf
# ./scripts/rls.sh sfx lang eng no-cm no-hl no-dd no-fnt no-smb no-pf # ./scripts/rls.sh sfx lang eng no-cm no-hl no-fnt no-smb no-pf
# (reduces v1.14.2 from 700k to 495k) # (reduces v1.14.2 from 700k to 495k)
EOF EOF
@ -76,7 +74,6 @@ gtar=$(command -v gtar || command -v gnutar) || true
sed() { gsed "$@"; } sed() { gsed "$@"; }
find() { gfind "$@"; } find() { gfind "$@"; }
sort() { gsort "$@"; } sort() { gsort "$@"; }
shuf() { gshuf "$@"; }
nproc() { gnproc; } nproc() { gnproc; }
sha1sum() { shasum "$@"; } sha1sum() { shasum "$@"; }
unexpand() { gunexpand "$@"; } unexpand() { gunexpand "$@"; }
@ -123,7 +120,6 @@ while [ ! -z "$1" ]; do
no-pf) no_pf=1 ; ;; no-pf) no_pf=1 ; ;;
no-fnt) no_fnt=1 ; ;; no-fnt) no_fnt=1 ; ;;
no-hl) no_hl=1 ; ;; no-hl) no_hl=1 ; ;;
no-dd) no_dd=1 ; ;;
no-cm) no_cm=1 ; ;; no-cm) no_cm=1 ; ;;
dl-wd) dl_wd=1 ; ;; dl-wd) dl_wd=1 ; ;;
ign-wd) ign_wd=1 ; ;; ign-wd) ign_wd=1 ; ;;
@ -160,9 +156,9 @@ stamp=$(
done | sort | tail -n 1 | sha1sum | cut -c-16 done | sort | tail -n 1 | sha1sum | cut -c-16
) )
rm -rf sfx$CSN/* rm -rf sfx/*
mkdir -p sfx$CSN build mkdir -p sfx build
cd sfx$CSN cd sfx
tmpdir="$( tmpdir="$(
printf '%s\n' "$TMPDIR" /tmp | printf '%s\n' "$TMPDIR" /tmp |
@ -221,6 +217,7 @@ necho() {
tar -zxf $f tar -zxf $f
mv pyftpdlib-*/pyftpdlib . mv pyftpdlib-*/pyftpdlib .
rm -rf pyftpdlib-* pyftpdlib/test rm -rf pyftpdlib-* pyftpdlib/test
patch -p1 <../scripts/patches/pyftpdlib-win313.patch
for f in pyftpdlib/_async{hat,ore}.py; do for f in pyftpdlib/_async{hat,ore}.py; do
[ -e "$f" ] || continue; [ -e "$f" ] || continue;
iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f iawk 'NR<4||NR>27||!/^#/;NR==4{print"# license: https://opensource.org/licenses/ISC\n"}' $f
@ -398,7 +395,7 @@ ts=$(date -u +%s)
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx) hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
mkdir -p ../dist mkdir -p ../dist
sfx_out=../dist/copyparty-sfx$CSN sfx_out=../dist/copyparty-sfx
echo cleanup echo cleanup
find -name '*.pyc' -delete find -name '*.pyc' -delete
@ -456,13 +453,6 @@ rm -f ftp/pyftpdlib/{__main__,prefork}.py
ised "s/src:.*scp.*\)/src:local('Consolas')/" $f ised "s/src:.*scp.*\)/src:local('Consolas')/" $f
} }
[ $no_dd ] && {
rm -rf copyparty/web/dd
f=copyparty/web/browser.css
gzip -d "$f.gz" || true
ised 's/(cursor: ?)url\([^)]+\), ?(pointer)/\1\2/; s/[0-9]+% \{cursor:[^}]+\}//; s/animation: ?cursor[^};]+//' $f
}
[ $langs ] && { [ $langs ] && {
echo $langs | grep -q eng || { echo $langs | grep -q eng || {
langs="eng|$langs" langs="eng|$langs"
@ -564,7 +554,7 @@ gzres() {
} }
zdir="$tmpdir/cpp-mksfx$CSN" zdir="$tmpdir/cpp-mksfx"
[ -e "$zdir/$stamp" ] || rm -rf "$zdir" [ -e "$zdir/$stamp" ] || rm -rf "$zdir"
mkdir -p "$zdir" mkdir -p "$zdir"
echo a > "$zdir/$stamp" echo a > "$zdir/$stamp"
@ -593,15 +583,7 @@ echo gen tarlist
for d in copyparty partftpy magic j2 py2 py37 ftp; do find $d -type f || true; done | # strip_hints for d in copyparty partftpy magic j2 py2 py37 ftp; do find $d -type f || true; done | # strip_hints
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort | sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1 sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
(grep -vE '\.gz$' list1; grep -E '\.gz$' list1) >list
for n in {1..50}; do
(grep -vE '\.gz$' list1; grep -E '\.gz$' list1 | (shuf||gshuf) ) >list || true
s=$( (sha1sum||shasum) < list | cut -c-16)
grep -q $s "$zdir/h" 2>/dev/null && continue
echo $s >> "$zdir/h"
break
done
[ $n -eq 50 ] && exit
echo creating tar echo creating tar
tar -cf tar "${targs[@]}" --numeric-owner -T list tar -cf tar "${targs[@]}" --numeric-owner -T list

View file

@ -0,0 +1,41 @@
Date: Tue, 22 Oct 2024 12:47:30 +0200
Subject: Workaround for isabs() on Windows + Python 3.13 (#652)
Starting from Python 3.13, `os.path.isabs("/foo")` on Windows return `False`
diff --git a/pyftpdlib/filesystems.py b/pyftpdlib/filesystems.py
index 9b9326bf..320ffe40 100644
--- a/pyftpdlib/filesystems.py
+++ b/pyftpdlib/filesystems.py
@@ -132,6 +132,16 @@ def cwd(self, path):
# --- Pathname / conversion utilities
+ @staticmethod
+ def _isabs(path, _windows=os.name == "nt"):
+ # Windows + Python 3.13: isabs() changed so that a path
+ # starting with "/" is no longer considered absolute.
+ # https://github.com/python/cpython/issues/44626
+ # https://github.com/python/cpython/pull/113829/
+ if _windows and path.startswith("/"):
+ return True
+ return os.path.isabs(path)
+
def ftpnorm(self, ftppath):
"""Normalize a "virtual" ftp pathname (typically the raw string
coming from client) depending on the current working directory.
@@ -146,3 +156,3 @@
assert isinstance(ftppath, unicode), ftppath
- if os.path.isabs(ftppath):
+ if self._isabs(ftppath):
p = os.path.normpath(ftppath)
@@ -162,3 +172,3 @@
# This is for extra protection, maybe not really necessary.
- if not os.path.isabs(p):
+ if not self._isabs(p):
p = u("/")
@@ -201,3 +211,3 @@
assert isinstance(fspath, unicode), fspath
- if os.path.isabs(fspath):
+ if self._isabs(fspath):
p = os.path.normpath(fspath)

View file

@ -22,10 +22,27 @@ update_arch_pkgbuild() {
rm -rf x rm -rf x
} }
update_mpr_pkgbuild() {
cd "$self/../contrib/package/makedeb-mpr"
rm -rf x
mkdir x
sha=$(sha256sum "$self/../dist/copyparty-$ver.tar.gz" | awk '{print$1}')
awk -v ver=$ver -v sha=$sha '
/^pkgver=/{sub(/[0-9\.]+/,ver)};
/^sha256sums=/{sub(/[0-9a-f]{64}/,sha)};
1' PKGBUILD >a
mv a PKGBUILD
rm -rf x
}
update_nixos_pin() { update_nixos_pin() {
( cd $self/../contrib/package/nix/copyparty; ( cd $self/../contrib/package/nix/copyparty;
./update.py $self/../dist/copyparty-sfx.py ) ./update.py $self/../dist/copyparty-sfx.py )
} }
update_arch_pkgbuild update_arch_pkgbuild
update_mpr_pkgbuild
update_nixos_pin update_nixos_pin

View file

@ -14,7 +14,6 @@ clean=--clean
uname -s | grep WOW64 && m=64 || m=32 uname -s | grep WOW64 && m=64 || m=32
uname -s | grep NT-10 && w10=1 || w7=1 uname -s | grep NT-10 && w10=1 || w7=1
[ $w7 ] && export PRTY_NO_MAGIC=1
[ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh [ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh
[ $w7 ] && pyv=37 || pyv=313 [ $w7 ] && pyv=37 || pyv=313

View file

@ -30,5 +30,5 @@ a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac7
3e39ea6e16b502d99a2e6544579095d0f7c6097761cd85135d5e929b9dec1b32e80669a846f94ee8c2cca9be2f5fe728625d09453988864c04e16bb8445c3f91 pillow-11.3.0-cp313-cp313-win_amd64.whl 3e39ea6e16b502d99a2e6544579095d0f7c6097761cd85135d5e929b9dec1b32e80669a846f94ee8c2cca9be2f5fe728625d09453988864c04e16bb8445c3f91 pillow-11.3.0-cp313-cp313-win_amd64.whl
59fbbcae044f4ee73d203ac74b553b27bfad3e6b2f3fb290fd3f8774753c6b545176b6b3399c240b092d131d152290ce732750accd962dc1e48e930be85f5e53 pyinstaller-6.14.1-py3-none-win_amd64.whl 59fbbcae044f4ee73d203ac74b553b27bfad3e6b2f3fb290fd3f8774753c6b545176b6b3399c240b092d131d152290ce732750accd962dc1e48e930be85f5e53 pyinstaller-6.14.1-py3-none-win_amd64.whl
fc6f3e144c5f5b662412de07cb8bf0c2eb3b3be21d19ec448aef3c4244d779b9ab8027fd67a4871e6e13823b248ea0f5a7a9241a53aef30f3b51a6d3cb5bdb3f pyinstaller_hooks_contrib-2025.5-py3-none-any.whl fc6f3e144c5f5b662412de07cb8bf0c2eb3b3be21d19ec448aef3c4244d779b9ab8027fd67a4871e6e13823b248ea0f5a7a9241a53aef30f3b51a6d3cb5bdb3f pyinstaller_hooks_contrib-2025.5-py3-none-any.whl
2c7a52e223b8186c21009d3fa5ed6a856d8eb4ef3b98f5d24c378c6a1afbfa1378bd7a51d6addc500e263d7989efb544c862bf920055e740f137c702dfd9d18b python-3.13.5-amd64.exe 36db028e9f3d6805a57e89320283c07bd5eb0bb15c6edcd2ae4a7e46b06bfe6c96ed0793e8936cbb09b4f6b680a3f06dace2220a1e7d8b74ab6047698871db9e python-3.13.7-amd64.exe
2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl 2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl

View file

@ -40,7 +40,7 @@ fns=(
pillow-11.3.0-cp313-cp313-win_amd64.whl pillow-11.3.0-cp313-cp313-win_amd64.whl
pyinstaller-6.14.1-py3-none-win_amd64.whl pyinstaller-6.14.1-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2025.5-py3-none-any.whl pyinstaller_hooks_contrib-2025.5-py3-none-any.whl
python-3.13.5-amd64.exe python-3.13.7-amd64.exe
setuptools-80.9.0-py3-none-any.whl setuptools-80.9.0-py3-none-any.whl
) )
[ $w7 ] && fns+=( [ $w7 ] && fns+=(

View file

@ -32,11 +32,8 @@ v=$1
rm -f ../dist/copyparty-sfx* rm -f ../dist/copyparty-sfx*
shift shift
./make-sfx.sh "$@" ./make-sfx.sh "$@"
f=../dist/copyparty-sfx ../dist/copyparty-sfx.py --version >/dev/null
[ -e $f.py ] && s= || s=-gz mv ../dist/copyparty-{sfx,int}.py
# TODO: the -gz suffix is gone, can drop all the $s stuff probably
$f$s.py --version >/dev/null
while [ "$1" ]; do while [ "$1" ]; do
case "$1" in case "$1" in
@ -46,27 +43,10 @@ while [ "$1" ]; do
shift shift
done done
[ $parallel -gt 1 ] && { ./make-pyz.sh
printf '\033[%s' s 2r H "0;1;37;44mbruteforcing sfx size -- press enter to terminate" K u "7m $* " K $'27m\n'
trap "rm -f .sfx-run; printf '\033[%s' s r u" INT TERM EXIT
touch .sfx-run
min=99999999
for ((a=0; a<$parallel; a++)); do
while [ -e .sfx-run ]; do
CSN=$a ./make-sfx.sh re "$@"
sz=$(wc -c <$f$a$s.py | awk '{print$1}')
[ $sz -ge $min ] && continue
mv $f$a$s.py $f$s.py.$sz
min=$sz
done &
done
read
exit
}
while true; do ./make-sfx.sh re lang eng "$@"
mv $f$s.py $f$s.$(wc -c <$f$s.py | awk '{print$1}').py mv ../dist/copyparty-{sfx,en}.py
./make-sfx.sh re "$@" mv ../dist/copyparty-{int,sfx}.py
done
# git tag -d v$v; git push --delete origin v$v # git tag -d v$v; git push --delete origin v$v

View file

@ -73,12 +73,6 @@ copyparty/web/browser.js,
copyparty/web/browser2.html, copyparty/web/browser2.html,
copyparty/web/cf.html, copyparty/web/cf.html,
copyparty/web/copyparty.gif, copyparty/web/copyparty.gif,
copyparty/web/dd,
copyparty/web/dd/2.png,
copyparty/web/dd/3.png,
copyparty/web/dd/4.png,
copyparty/web/dd/5.png,
copyparty/web/dd/__init__.py,
copyparty/web/deps, copyparty/web/deps,
copyparty/web/deps/__init__.py, copyparty/web/deps/__init__.py,
copyparty/web/deps/busy.mp3, copyparty/web/deps/busy.mp3,

View file

@ -81,6 +81,7 @@ var tl_cpanel = {
"ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!", "ad1": "enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!",
"ae1": "active downloads:", "ae1": "active downloads:",
"af1": "show recent uploads", "af1": "show recent uploads",
"ag1": "view idp cache",
}, },
}; };
@ -315,6 +316,7 @@ var tl_browser = {
"ct_qdel": 'when deleting files, only ask for confirmation once">qdel', "ct_qdel": 'when deleting files, only ask for confirmation once">qdel',
"ct_dir1st": 'sort folders before files">📁 first', "ct_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort', "ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_utc": 'show all datetimes in UTC">UTC',
"ct_readme": 'show README.md in folder listings">📜 readme', "ct_readme": 'show README.md in folder listings">📜 readme',
"ct_idxh": 'show index.html instead of folder listing">htm', "ct_idxh": 'show index.html instead of folder listing">htm',
"ct_sbars": 'show scrollbars">⟊', "ct_sbars": 'show scrollbars">⟊',
@ -364,6 +366,7 @@ var tl_browser = {
"ml_drc": "dynamic range compressor", "ml_drc": "dynamic range compressor",
"mt_loop": "loop/repeat one song\">🔁", "mt_loop": "loop/repeat one song\">🔁",
"mt_one": "stop after one song\">1⃣",
"mt_shuf": "shuffle the songs in each folder\">🔀", "mt_shuf": "shuffle the songs in each folder\">🔀",
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶", "mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
"mt_preload": "start loading the next song near the end for gapless playback\">preload", "mt_preload": "start loading the next song near the end for gapless playback\">preload",
@ -381,6 +384,7 @@ var tl_browser = {
"mt_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache", "mt_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
"mt_mloop": "loop the open folder\">🔁 loop", "mt_mloop": "loop the open folder\">🔁 loop",
"mt_mnext": "load the next folder and continue\">📂 next", "mt_mnext": "load the next folder and continue\">📂 next",
"mt_mstop": "stop playback\">⏸ stop",
"mt_cflac": "convert flac / wav to opus\">flac", "mt_cflac": "convert flac / wav to opus\">flac",
"mt_caac": "convert aac / m4a to opus\">aac", "mt_caac": "convert aac / m4a to opus\">aac",
"mt_coth": "convert all others (not mp3) to opus\">oth", "mt_coth": "convert all others (not mp3) to opus\">oth",
@ -388,6 +392,8 @@ var tl_browser = {
"mt_c2owa": "opus-weba, for iOS 17.5 and newer\">owa", "mt_c2owa": "opus-weba, for iOS 17.5 and newer\">owa",
"mt_c2caf": "opus-caf, for iOS 11 through 17\">caf", "mt_c2caf": "opus-caf, for iOS 11 through 17\">caf",
"mt_c2mp3": "use this on very old devices\">mp3", "mt_c2mp3": "use this on very old devices\">mp3",
"mt_c2flac": "best sound quality, but huge downloads\">flac",
"mt_c2wav": "uncompressed playback (even bigger)\">wav",
"mt_c2ok": "nice, good choice", "mt_c2ok": "nice, good choice",
"mt_c2nd": "that's not the recommended output format for your device, but that's fine", "mt_c2nd": "that's not the recommended output format for your device, but that's fine",
"mt_c2ng": "your device does not seem to support this output format, but let's try anyways", "mt_c2ng": "your device does not seem to support this output format, but let's try anyways",

View file

@ -132,11 +132,11 @@ args = {
"copyparty.stolen.ifaddr", "copyparty.stolen.ifaddr",
"copyparty.web", "copyparty.web",
"copyparty.web.a", "copyparty.web.a",
"copyparty.web.dd",
"copyparty.web.deps", "copyparty.web.deps",
], ],
"install_requires": ["jinja2"], "install_requires": ["jinja2"],
"extras_require": { "extras_require": {
"all": ["argon2-cffi", "partftpy>=0.4.0", "Pillow", "pyftpdlib", "pyopenssl", "pyzmq"],
"thumbnails": ["Pillow"], "thumbnails": ["Pillow"],
"thumbnails2": ["pyvips"], "thumbnails2": ["pyvips"],
"audiotags": ["mutagen"], "audiotags": ["mutagen"],

View file

@ -63,7 +63,7 @@ class TestVFS(unittest.TestCase):
cfgdir = os.path.join(here, "res", "idp") cfgdir = os.path.join(here, "res", "idp")
# globals are applied by main so need to cheat a little # globals are applied by main so need to cheat a little
xcfg = {"idp_h_usr": "x-idp-user", "idp_h_grp": "x-idp-group"} xcfg = {"idp_h_usr": ["x-idp-user"], "idp_h_grp": "x-idp-group"}
return here, cfgdir, xcfg return here, cfgdir, xcfg

View file

@ -143,7 +143,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "chpw daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz rmagic rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip" ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip"
@ -152,22 +152,25 @@ class Cfg(Namespace):
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip" ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
ka.update(**{k: None for k in ex.split()}) ka.update(**{k: None for k in ex.split()})
ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz" ex = "gid uid"
ka.update(**{k: -1 for k in ex.split()})
ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who"
ka.update(**{k: 1 for k in ex.split()}) ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
ka.update(**{k: 9 for k in ex.split()}) ka.update(**{k: 9 for k in ex.split()})
ex = "db_act forget_ip idp_store k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs" ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
ka.update(**{k: 0 for k in ex.split()}) ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR" ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles txt_eol unlist vname xff_src zipmaxt R RS SR"
ka.update(**{k: "" for k in ex.split()}) ka.update(**{k: "" for k in ex.split()})
ex = "ban_403 ban_404 ban_422 ban_pw ban_url spinner" ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner"
ka.update(**{k: "no" for k in ex.split()}) ka.update(**{k: "no" for k in ex.split()})
ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()}) ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md" ex = "exp_lg exp_md"
@ -182,9 +185,12 @@ class Cfg(Namespace):
E=E, E=E,
bup_ck="sha512", bup_ck="sha512",
chmod_d="755", chmod_d="755",
cookie_cmax=8192,
cookie_nmax=50,
dbd="wal", dbd="wal",
dk_salt="b" * 16, dk_salt="b" * 16,
fk_salt="a" * 16, fk_salt="a" * 16,
grp_all="acct",
idp_gsep=re.compile("[|:;+,]"), idp_gsep=re.compile("[|:;+,]"),
iobuf=256 * 1024, iobuf=256 * 1024,
lang="eng", lang="eng",