Compare commits

...

417 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
ed 4dca1cf8f4 v1.18.4 2025-07-25 18:41:05 +00:00
ed edba7fffd3 add landmarks (#182) 2025-07-25 18:35:28 +00:00
ed 21a96bcfe8 add quickdelete option; closes #183
togglebutton in the ui switches between 2 (off/default) and
1 (on/quick) confirmations; global-option `--qdel` sets the default

setting `--qdel=0` changes the togglebutton to switch
between 1 (off/default) confirmations and 0 (on)

in other words, when the ui-button is enabled, it
always reduces the number of confirmations by one
2025-07-25 18:31:49 +00:00
ed 2d322dd48e fix unpost in new shares 2025-07-25 15:12:05 +00:00
ed df6d4df4f8 fix filekeys on windows 2025-07-24 23:07:04 +00:00
ed 5aa893973c update pkgs to 1.18.3 2025-07-21 23:30:16 +00:00
ed be0dd555a6 v1.18.3 2025-07-21 23:07:00 +00:00
ed 9921c43e3a add options to set default chmod (#181)
the unix-permissions of new files/folders can now be changed

* global-option --chmod-f, volflag chmod_f for files
* global-option --chmod-d, volflag chmod_d for directories

the expected value is a standard three-digit octal value
(User/Group/Other) such as 755, 750, 644, 640, etc
2025-07-21 22:46:28 +00:00
ed 14fa369fae macos fixes 2025-07-21 00:04:38 +02:00
ed 0f0f8d90c1 support --shr with --xvol; closes #179 2025-07-20 23:49:36 +02:00
ed 1afbff7335 fix some error-messages failing to render
would show a jinja-panic instead of explaining what went wrong
2025-07-20 23:39:08 +02:00
ed 8c32b0e7bb bbox: hide buttons fully; closes #180 2025-07-20 23:31:38 +02:00
ed 9bc4c5d2e6 mediaplayer: stay within search-results 2025-07-20 23:30:27 +02:00
ed 1534b7cb55 fix hotkey-help on macos 2025-07-20 23:27:44 +02:00
ed 56d3bcf515 rss: fix --rp-loc;
some rss links were malformed when combined with rp-loc
2025-07-14 03:48:27 +02:00
ed 78605d9a79 ios: force video embed
default on all other platforms, but apple thinks different
2025-07-09 14:11:45 +00:00
ed d46a40fed8 update pkgs to 1.18.2 2025-07-07 14:29:38 +00:00
ed ce4e489802 v1.18.2 2025-07-07 14:19:56 +00:00
ed fd7c71d6a3 add volflag to hide volume from controlpanel listing 2025-07-07 14:15:58 +00:00
ed fad2268566 update pkgs to 1.18.1 2025-07-07 13:39:55 +00:00
ed a95ea03cd0 v1.18.1 2025-07-07 13:20:59 +00:00
ed f6be390579 avoid pillow warning 2025-07-07 12:58:03 +00:00
ed 4f264a0a9c add idp-cache editor ui 2025-07-07 12:52:31 +00:00
ed d27144340f ie11 fix 2025-07-07 11:09:46 +00:00
ed 299cff3ff7 copyparty.exe: update pillow 2025-07-07 11:05:49 +00:00
ed 42c199e78e api for rescanning multiple volumes;
`?scan=/foo,/bar` will perform a filesystem reindexing of volumes
`/foo` and `/bar` even if they only have `e2d` and not `e2ds`
2025-07-07 09:53:03 +00:00
ed 1b2d39857b reset x-forwarded-for before next req;
assume the following stack: cpp <- rproxyA <- rproxyB <- WAN

if A also accepts WAN requests, and A muxes both B and WAN
onto a single connection to cpp, then WAN requests may get
tagged with the IP-address of the most recent B request

aside from the confusing logs, this could break
unpost on servers with shared accounts
2025-07-07 08:47:24 +00:00
ed ed908b9868 usb-eject: support non-alphanumeric volume names
until now, volumes with whitespace and such would fail to unmount

also adds a sanchk that the directory to unmount is still below the
expected parent after absreal; the path was already passed to gio in
a safe manner (assuming gio doesn't have any vulns) but why risk it
2025-07-07 08:35:41 +00:00
ed d162502c38 add idp-volume persistence (optional);
it keeps track of all seen users/groups by default,
but nothing takes effect unless --idp-store=3 or 2
2025-07-07 01:05:57 +02:00
ed bf11b2a421 drop corrupted sockets;
socket.accept() can fail silently --
this would crash the worker-pool and also produce
a confusing useless error-message while doing so

reported by someone on a mac with Little Snitch:
uv python install cpython-3.13.3-macos-aarch64-none
uv python pin cpython-3.13.3-macos-aarch64-none
uv sync
uv run copyparty

...but was also observed on x86_64 linux with
python 2.7 in 2018 (no longer reproduces)

fix this to log what's going on and also don't crash
2025-07-01 18:32:27 +00:00
morganamilo 77274e9d59 Add python-magic to iv and dj docker files 2025-06-29 11:14:02 +00:00
ed 8306e3d9de docker: disarm unmaintained images 2025-06-29 11:13:29 +00:00
ed deb6711b51 docker: add missing cleanup 2025-06-29 11:12:29 +00:00
ed 7ef6fd13cf navpane: fix scrollbar overlap 2025-06-28 21:10:48 +00:00
ed 65c4e03574 fix keyfinder build;
stopped working in alpine 3.22 due to switching to llvm,
which strictly requres CXXFLAGS rather than CFLAGS

the PKG_CONFIG_PATH change is unnecessary but might as well
2025-06-22 12:27:11 +00:00
ed c9fafb202d copyparty32.exe: fix segfault on win7 2025-06-22 01:17:48 +00:00
ed d4d9069130 update pkgs to 1.18.0 2025-06-22 00:59:42 +00:00
ed 7eca90cc21 v1.18.0 2025-06-22 00:20:31 +00:00
ed 6ecf4fdceb textfile-streaming fixes;
* add optional max duration, default-infinite
* add optional wordwrap, default-enabled
* url-param `...&tail` enables tailing in textviewer too
* hide bottom tray while tailing
2025-06-21 23:36:19 +00:00
ed 8cae7a715b fix linecrop bleed (#170):
chrome (only on windows and macos) could show the top
row of pixels of the truncated line; this seems to fix it
2025-06-20 16:55:47 +02:00
ed c75b0c25a6 ext-th: reduce specificity (#170);
thumbnails defined for file-extension '.asdf' will now also
apply to '.qwer.asdf' if no more specific ext-th is given
2025-06-20 16:25:30 +02:00
ed 9dd5dec093 adjustments after #171;
* move the new functionality to --rmagic
* performance tweaks
2025-06-19 17:25:31 +00:00
morganamilo ec05f8ccd5 Detect content-type when extension is missing or unknown
If a file has no known extension the content type gets set to
application/octet-stream causing the browser try and download the file
when viewed directly.

This quickly becomes annoying as many of the files I interact with often
have no extension. I.e., config files, log files, LICENSE files and
other random text files.

This patch uses libmagic to detect the file type and set the
content-type header. It also does this for the RSS feed and webdav for
sake of completeness.

This patch does not touch the front end at all so these files still have a 'txt'
button and a type of '%' in the web UI. But when clicked on, the browser
will display the files correctly.

This feature is enabled with the existing "magic" option. I thought this
fit as the existing functionality also uses libmagic and gives file
extensions to files on upload. Tell me if it should be its own option
instead.

The code base was very confusing, this patch works but I have no idea if
it's the way you'd like this implemented. Hopefully its acceptable as
is.
2025-06-19 17:18:23 +00:00
ed a1c7a095ee textfile-streaming fixes;
* give up on disconnect
* block scrapers from tailing
* prism throws on window-resize if riced object has poofed
* fix prism-init race
2025-06-19 17:07:06 +00:00
ed 77df17d191 add ui for streaming textfiles in realtime 2025-06-16 00:00:40 +00:00
ed fa5845ff5f readme: explain ext-th better; closes #170 2025-06-14 22:38:04 +00:00
ed 17fa490687 add ?tail 2025-06-14 21:13:14 +00:00
ed 1eff87c3bd copyparty.exe: upgrade to python 3.13 2025-06-13 21:53:16 +00:00
ed d123d2bff0 add test for non-idp group filtering 2025-06-13 19:34:58 +00:00
ed 5ac3864874 avoid new SyntaxWarning in python 3.14
this change should not alter behavior; the code was already correct

prevents the following message on stdout during startup:
SyntaxWarning: 'return' in a 'finally' block
2025-06-08 18:32:45 +02:00
ed c599e2aaa3 add opt for dotfile visibility default 2025-06-08 18:32:32 +02:00
ed 2e53f7979a IdP: multiple group rules for ${u} and ${g}
until now, ${u} would match all users,
${u%-foo} would exclude users in group foo,
${u%+foo} would only include users in group foo

now, the following is also possible:
${u%-foo,%-bar} excludes users in group foo and/or group bar,
${u%+foo,%+bar} only includes users which are in groups foo AND bar,
${g%-foo} skips group foo (includes all others),
${g%-foo,%-bar} skips group foo and/or bar (includes all others)

see ./docs/examples/docker/idp/copyparty.conf ;
https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/idp/copyparty.conf
2025-06-03 20:03:17 +00:00
ed f61511d8c8 docs: building from source / building from scratch 2025-05-29 21:54:54 +00:00
ed 47415a7120 update pkgs to 1.17.2 2025-05-27 20:11:24 +00:00
ed db7becacd2 v1.17.2 2025-05-27 19:39:22 +00:00
ed 28b63e587b docker: improve lack-of-config panic 2025-05-27 18:52:41 +00:00
ed 9cb93ae1ed fix upload into share with vproxy; closes #168 2025-05-27 16:29:03 +00:00
ed e3e51fb83a mitigate google-chrome slow hashing
file hashing became drastically slower in recent chrome versions;

* 748 MiB/s in 131.0.6778.86
* 747 MiB/s in 132.0.6834.160
* 485 MiB/s in 133.0.6943.60
* 319 MiB/s in 134.0.6998.36

the silver lining: it looks like chrome-bug 1352210 is improving
(crypto.subtle, the native hasher, now scales with multiple cores)

* 133.0.6943.60: speed peaked at 2 threads; 341 MiB/s, 485 MiB/s
* 134.0.6998.36: peak at 7; 193, 383, 383, 408, 421, 431, 438, 438
* 137.0.7151.41: peak at 8; 210, 382, 445, 513, 573, 573, 585, 598
   MiB/s when hashing with 1, 2, ..., 7, 8 webworkers respectively
   on a ryzen7-5800x with 2x16g 2133mhz ram

characteristics of versions between v134 and v137 are unknown
(cannot find old official builds to test), but v137 is a good
cutoff for minimizing risk of hitting chrome-bugs

meanwhile, hash-wasm scales linearly up to 8 cores;
0=328 1=377 2=738 3=947 4=1090 5=1190 6=1380 7=1530 8=1810
(0 = wasm on mainthread, no webworkers)

but it looks like chrome-bug 383568268 is making a return,
so keep the limit of max 4 threads if machine has more than
4 cores (and numCores-1 otherwise)
2025-05-27 15:33:50 +00:00
ed 49c7124776 fix errorhandling for browser-oom
because chrome-bug 383568268 is possibly making a return soon
(observed in google-chrome 138.0.7191.0 and chromium 139.0.7205.0)
2025-05-27 15:25:09 +00:00
Harsh Shandilya 60fb1207fc fix: disable use of aliases in nixpkgs
This enables compatibility with users who also disable aliases

The utillinux alias was added in 2020[1], which is older than the previous
Nixpkgs pin, which means we can safely switch to the non-aliased version.

1: 3896a0c0e2/pkgs/top-level/aliases.nix (L1967)
2025-05-27 10:17:15 +00:00
Harsh Shandilya 48470f6b50 fix: update to the latest NixOS release
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/884e3b68be02ff9d61a042bc9bd9dd2a358f95da' (2023-04-01)
  → 'github:NixOS/nixpkgs/7c43f080a7f28b2774f3b3f43234ca11661bf334' (2025-05-25)
2025-05-27 10:17:15 +00:00
ed 1d308eeb4c minimal-up2k: add usage instructions 2025-05-21 20:53:19 +00:00
ed 84f5f41747 unconditionally apply --rp-loc (#165)
previously, `--rp-loc` only took effect for trusted reverse-proxies

this was a source of confusion when setting up a config from
scratch, since there is no obvious relation to `--xff-src`

as this behavior was incidental, `--rp-loc` is now always applied,
even if the proxy is untrusted (or not detected at all)
2025-05-19 22:01:29 +00:00
ed 19189afb34 docker: fix i386 builds 2025-05-18 23:49:41 +00:00
ed 23e77a3389 update pkgs to 1.17.1 2025-05-18 22:52:49 +00:00
ed ecced0c4f2 v1.17.1 2025-05-18 22:34:16 +00:00
ed d4a8071de5 add kde dolphin to connect-page
mentions the specific protocol (webdav/webdavs) to use, #162
2025-05-18 22:07:03 +00:00
ed 261236e302 st_mtime can be -11644473600 on win64 fat16 vhd 2025-05-18 21:34:38 +00:00
ed 0de09860f6 new option: default-hasher for PUTs 2025-05-17 16:55:29 +02:00
ed bfb39969a4 macos: fix test race 2025-05-16 12:28:34 +02:00
ed 256dad8cc0 button to zip/tar current folder 2025-05-14 18:02:38 +02:00
ed a247ba9ca3 update translations 2025-05-14 17:51:33 +02:00
ed 0a9a807772 fix xbu/xau reloc collision-handling;
if a hook relocates a file into a folder where that same file
exists with the same filename, the filename-collision-avoidance
would kick in, generating a new filename and another copy
2025-05-14 15:45:52 +02:00
ed 41fa6b2552 improve tagscan-resume for dupes;
* ignore t:mtp (the todo-flag) when spooling the resume-list
* only add a single t:mtp for each unique file
2025-05-14 12:32:30 +02:00
ed f425ff51ae cross-filesystem-move fixes
* nonlocal markdown backups
* relocation-hooks

tested on macos, to be verified on Linux/windows
2025-05-14 12:30:59 +02:00
ed 7cde9a2976 alias .oga to .ogg
because firefox renames .ogg files to .oga when saving
2025-05-12 18:50:29 +02:00
ed 5dcd88a6c8 add option --put-name; closes #164 2025-05-12 10:30:41 +02:00
ed c3ef3fdc1f fix --shr with pw-hash; closes #162
--ah-alg now also applies to password-protected shares
2025-05-11 20:10:00 +02:00
ed b9ba783c1c official archlinux package 2025-05-05 21:25:52 +02:00
Gabriel Venberg d1bca1f52f
nixos: revamp (#159)
* formatting clean-up with alejandra.

* added ability to specify user and group.

* added option to have hist data live with volumes.

* improved my understanding of what paths copyparty needs to function.

* added environment script.

* Revert "added environment script."

Cant have 2 instances of copyparty running, even if one is just for
ah-cli...

This reverts commit c60c8d8e0b.

* fixup! added ability to specify user and group.

* Reapply "added environment script."

This reverts commit a54e950ecc.

* Moved back to TemporaryFileSystem for system hardening.

I misunderstood bind mounts...

* made systemd.tmpfiles rules to ensure the volume directories exist.

* changed copyparty-env script to copyparty-hash.

* removed seperatehist in favor of default settings attrset.

* new update of copyparty removed the need for some options.

* minor refactoring.

* fixed some descriptions that had not kept up with changes.

* fixup! removed seperatehist in favor of default settings attrset.
2025-04-29 14:48:17 +02:00
ed 94352f278b non-https clipboard newlines; fixes #161 2025-04-28 19:00:13 +00:00
ed 4fb87ebe32 flatcase best case 2025-04-27 09:25:01 +00:00
ed 3cbb7243ab update pkgs to 1.17.0 2025-04-26 22:50:45 +00:00
ed fff45552da v1.17.0 2025-04-26 21:49:09 +00:00
ed 95157d02c9 ie11 can't sandbox; add minimal fallback 2025-04-26 20:14:23 +00:00
ed 3090c74832 ie11: fix debounce-untint;
css 'unset' appeared in chr41, ff27

dom.closest appeared in chr41, ff35
2025-04-26 19:57:59 +00:00
ed 4195762d2a playlist: when lacking perms, s/edit/view/ 2025-04-26 19:28:12 +00:00
ed dc3b7a2720 reduce --th-ram-max floor;
helps avoid oom in a vm with 512 MiB ram
2025-04-26 19:06:32 +00:00
ed ad200f2b97 add ui for creating playlists 2025-04-26 00:19:41 +00:00
ed 897f9d328d audioplayer: load and play m3u8 playlists 2025-04-25 22:33:00 +00:00
ed efbe34f29d readme: mention basic-auth behavior 2025-04-25 18:57:12 +00:00
ed dbfc899d79 pw-hash tweaks (#159):
* do not take lock on shares-db / sessions-db when running with
   `--ah-gen` or `--ah-cli` (allows a 2nd instance for that purpose)

* add options to print effective salt for ah/fk/dk; useful for nixos
   and other usecases where config is derived or otherwise opaque
2025-04-25 18:12:35 +00:00
ed 74fb4b0cb8 fix --u2j helptext:
* mention potential hdd-bottleneck from big values
* most browsers enforce a max-value of 6 (c354a38b)
* chunk-stitching (132a8350) made this less important;
   still beneficial, but only to a point
2025-04-24 20:51:45 +00:00
ed 68e7000275 update pkgs to 1.16.21 2025-04-20 19:19:35 +00:00
ed 38c2dcce3e v1.16.21 2025-04-20 18:36:32 +00:00
ed 5b3a5fe76b show warning on ctrl-a in lazyloaded folders 2025-04-20 13:33:01 +00:00
ed d5a9bd80b2 docker: hide healthcheck from logs 2025-04-20 12:26:56 +00:00
ed 71c5565949 add button to loop/repeat music; closes #156 2025-04-20 11:45:48 +00:00
ed db33d68d42 zip-download: eagerly 64bit data-descriptors; closes #155
this avoids a false-positive in the info-zip unzip zipbomb detector.

unfortunately,

* now impossible to extract large (4 GiB) zipfiles using old software
   (WinXP, macos 10.12)

* now less viable to stream download-as-zip into a zipfile unpacker
   (please use download-as-tar for that purpose)

context:

the zipfile specification (APPNOTE.TXT) is slightly ambiguous as to when
data-descriptor (0x504b0708) filesize-fields change from 32bit to 64bit;
both copyparty and libarchive independently made the same interpretation
that this is only when the local header is zip64, AND the size-fields
are both 0xFFFFFFFF. This makes sense because the data descriptor is
only necessary when that particular file-to-be-added exceeds 4 GiB,
and/or when the crc32 is not known ahead of time.

another interpretation, seen in an early version of the patchset
to fix CVE-2019-13232 (zip-bombs) in the info-zip unzip command,
believes the only requirement is that the local header is zip64.

in many linux distributions, the unzip command would thus fail on
zipfiles created by copyparty, since they (by default) satisfy
the three requirements to hit the zipbomb false-positive:

* total filesize exceeds 4 GiB, and...
* a mix of regular (32bit) and zip64 entries, and...
* streaming-mode zipfile (not made with ?zip=crc)

this issue no longer exists in a more recent version of that patchset,
https://github.com/madler/unzip/commit/af0d07f95809653b
but this fix has not yet made it into most linux distros
2025-04-17 18:52:47 +00:00
ed e1c20c7a18 readme: mention bootable flashdrive / cdrom 2025-04-17 18:45:50 +00:00
ed d3f1b45ce3 update pkgs to 1.16.20 2025-04-13 22:32:06 +00:00
ed c7aa1a3558 v1.16.20 2025-04-13 21:51:39 +00:00
ed 7b2bd6da83 fix sorting of japanese folders
directory-tree sidebar did not sort correctly for non-ascii names

also fix a natural-sort bug; it only took effect for the
initial folder load, and not when changing the sort-order

also, natural-sort will now apply to all non-numeric fields,
not just the filename like before
2025-04-13 21:11:07 +00:00
ed 2bd955ba9f race-the-beam: improve phrasing 2025-04-13 18:51:45 +00:00
ed 98dcaee210 workaround ffmpeg-bug 10797
reduces ram usage from 1534 to 230 MiB when generating spectrograms
of files which are decoded by libopenmpt, so most s3xmodit formats
2025-04-13 18:51:35 +00:00
ed 361aebf877 warn on zeroconf with uds-only 2025-04-13 16:38:29 +00:00
ed ffc1610980 dont crash if qrcode + mdns + uds 2025-04-13 16:11:36 +00:00
ed 233075aee7 ctrl-a selects all files in gridview too 2025-04-13 16:09:49 +00:00
ed d1a4d335df increase treenav scroll-margins
was too small in deep folders, and/or long foldernames
2025-04-13 16:09:14 +00:00
ed 96acbd3593 cleanup
* remove cpr bonk (deadcode)
* remove get_vpath (wasteful)
2025-04-13 16:08:44 +00:00
thaddeus kuah 4b876dd133 full lowercase on login button to match the page
Signed-off-by: thaddeus kuah <tk@tkkr.dev>
2025-04-11 23:56:51 +02:00
ed a06c5eb048 new xau hook: podcast-normalizer.py 2025-04-09 19:44:13 +00:00
ed c9cdc3e1c1 update pkgs to 1.16.19 2025-04-08 21:52:43 +00:00
ed c0becc6418 v1.16.19 2025-04-08 21:32:51 +00:00
ed b17ccc38ee prefer XDG_CONFIG_HOME on all platforms
convenient escape-hatch
2025-04-08 19:23:14 +00:00
ed acfaacbd46 enforce single-instance for session/shares db
use file-locking to detect and prevent misconfigurations
which could lead to subtle unexpected behavior
2025-04-08 19:08:12 +00:00
ed 8e0364efad if this is wrong i blame suzubrah for playing entirely too hype music at 6am in the fkn morning
improve shares/session-db smoketests and error semantics
2025-04-08 05:42:21 +00:00
ed e3043004ba improve u2ow phrasing 2025-04-07 20:48:43 +00:00
ed b2aaf40a3e speedgolf
in some envs (unsure which), importlib.resources is an
expensive import; drop it when we know it's useless
2025-04-07 20:34:55 +00:00
ed 21db8833dc tests: fix for f9954bc4e5 2025-04-07 18:59:43 +00:00
ed ec14c3944e fix DeprecationWarning: Accessing argon2.__version__ is deprecated and will be removed in a future release. Use importlib.metadata directly to query for structlog's packaging metadata. 2025-04-07 18:51:13 +00:00
ed 20920e844f svg newlines + fix cleaner warning:
* support newlines in svg files;
  * `--error--\ncheck\nserver\nlog`
  * `upload\nonly`

* thumbnails of files with lastmodified year 1601 would
   make the cleaner print a harmless but annoying warning
2025-04-07 18:47:20 +00:00
ed f9954bc4e5 smoketest fs-access when transcoding
the thumbnailer / audio transcoder could return misleading errors
if the operation fails due to insufficient filesystem permissions

try reading a few bytes from the file and bail early if it fails,
and detect/log unwritable output folders for thumbnails

also fixes http-response to only return svg-formatted errors
if the initial request expects a picture in response, not audio
2025-04-07 18:41:37 +00:00
thaddeus kuah d450f61534
Apply custom fonts to buttons and input fields (#152)
* set custom font for inputs and buttons

Signed-off-by: Thaddeus Kuah <tk@tkkr.dev>
2025-04-06 19:15:10 +00:00
ed 2b50fc2010 fix mkdir in symlinked folders; closes #151
remove an overly careful safety-check which would refuse creating
directories if the location was outside of the volume's base-path

it is safe to trust `rem` due to `vpath = undot(vpath)` and
a similar check being performed inside `vfs.get` as well,
so this served no purpose
2025-04-06 09:18:40 +00:00
ed c2034f7bc5 add GoogleOther to bad-crawlers list 2025-04-01 21:29:58 +02:00
ed cec3bee020 forbid all use of LLM / AI when writing code 2025-03-31 17:25:56 +00:00
ed e1b9ac631f separate histpath and dbpath options (#149)
the up2k databases are, by default, stored in a `.hist` subfolder
inside each volume, next to thumbnails and transcoded audio

add a new option for storing the databases in a separate location,
making it possible to tune the underlying filesystem for optimal
performance characteristics

the `--hist` global-option and `hist` volflag still behave like
before, but `--dbpath` and volflag `dbpath` will override the
histpath for the up2k-db and up2k-snap exclusivey
2025-03-30 16:08:28 +00:00
ed 19ee64e5e3 clarify that all dependencies are optional (#149) 2025-03-30 13:30:52 +00:00
ed 4f397b9b5b add zfs-tune (#149) 2025-03-30 13:30:15 +00:00
ed 71775dcccb mention mimalloc 2025-03-30 13:17:12 +00:00
ed b383c08cc3 add review from ixbt forums 2025-03-29 13:57:35 +00:00
ed fc88341820 add option to store markdown backups elsewhere
`--md-hist` / volflag `md_hist` specifies where to put old
versions of markdown files when edited using the web-ui;

* `s` = create `.hist` subfolder next to the markdown file
   (the default, both previously and now)

* `v` = use the volume's hist-path, either according to
   `--hist` or the `hist` volflag. NOTE: old versions
   will not be retrievable through the web-ui

* `n` = nope / disabled; overwrite without backup
2025-03-26 20:07:35 +00:00
ed 43bbd566d7 mention mimalloc in docker-compose examples (thx thad) 2025-03-24 23:19:17 +00:00
ed e1dea7ef3e dangit 2025-03-23 23:28:05 +00:00
ed de2fedd2cd update pkgs to 1.16.18 2025-03-23 23:04:53 +00:00
ed 6aaafeee6d v1.16.18 2025-03-23 22:16:40 +00:00
ed 99f63adf58 google isn't taking the hint
specifically google, but also some others, have started ignoring
rel="nofollow" while also understanding just enough javascript to
try viewing binary files as text
2025-03-23 21:21:41 +00:00
ed de2c978842 docker: suggest mimalloc 2025-03-23 20:45:03 +00:00
ed 3c90cec0cd forgot these
pyinstaller/build.sh: fix jinja2 after upgrade

up2k.py: fix double-hs after dupe finalize
2025-03-23 20:19:18 +00:00
ed 57a56073d8 use zlib-ng when available
download-as-tar-gz becomes 2.4x faster in docker

segfaults on windows, so don't use it there

does not affect fedora or gentoo,
since zlib-ng is already system-default on those

also adds a global-option to write list of successful
binds to a textfile, for automation / smoketest purposes
2025-03-23 20:15:21 +00:00
ed 2525d594c5 19a5985f removed the restriction on uploading logues, as it was
too restrictive, blocking editing through webdav and ftp

but since logues and readmes can be used as helptext for users
with write-only access, it makes sense to block logue/readme
uploads from write-only users

users with write-only access can still upload any file as before,
but the filename prefix `_wo_` is added onto files named either
README.md | PREADME.md | .prologue.html | .epilogue.html

the new option `--wo-up-readme` restores previous behavior, and
will not add the filename-prefix for readmes/logues
2025-03-22 14:21:35 +00:00
ed a0ecc4d88e update pkgs to 1.16.17 2025-03-16 21:13:23 +00:00
ed accd003d15 v1.16.17 2025-03-16 20:02:51 +00:00
ed 9c2c423761 IdP: extend ${u} with syntax to exclude by group
just like before, if vpath contains ${u} then
the IdP-volume is created unconditionally

but this is new:

${u%+foo} creates the vol only if user is member of group foo

${u%-foo} creates the vol if user is NOT member of group foo
2025-03-16 19:28:23 +00:00
ed 999789c742 improve accuracy of failsafe-check
also fix fsutil relabel after 8417098c
2025-03-16 18:49:42 +00:00
ed 14bb299918 hide zip-link when user not allowed 2025-03-16 18:08:20 +00:00
ed 0a33336dd4 cosmetic: fix zipmax in up2k volume-listing 2025-03-16 17:51:50 +00:00
ed 6a2644fece set nofollow on ?doc links
google keeps trying to read binaries as text, maybe now it won't
2025-03-16 11:57:42 +00:00
ed 5ab09769e1 move symlinks as-is; don't expand into full files
previously, when moving or renaming a symlink to a file (or
a folder with symlinks inside), the dedup setting would decide
whether those links would be expanded into full files or not

with dedup disabled (which is the default),
all symlinks would be expanded during a move operation

now, the dedup-setting is ignored when files/folders are moved,
but it still applies when uploading or copying files/folders

* absolute symlinks are moved as-is

* relative symlinks are rewritten as necessary,
   assuming both source and destination is known in db
2025-03-15 23:54:32 +00:00
ed 782084056d filter appledoubles from uploads
should catch all the garbage that macs sprinkle onto flashdrives;
https://a.ocv.me/pub/stuff/?doc=appledoubles-and-friends.txt

will notice and suggest to skip the following files/dirs:

* __MACOSX
* .DS_Store
* .AppleDouble
* .LSOverride
* .DocumentRevisions-*
* .fseventsd
* .Spotlight-V*
* .TemporaryItems
* .Trashes
* .VolumeIcon.icns
* .com.apple.timemachine.donotpresent
* .AppleDB
* .AppleDesktop
* .apdisk

and conditionally ._foo.jpg if foo.jpg is also being uploaded
2025-03-15 21:16:54 +00:00
ed 494179bd1c optional max-size for download-as-zip/tar 2025-03-14 23:36:01 +00:00
ed 29a17ae2b7 fix detection of unsafe IdP volumes; closes #147
was overly aggressive until now, thinking the following was unsafe:

-v 'x::'                                  # no-anonymous-access
-v 'x/${u}:${u}:r:A,${u}'           # world-readable,user-admin
-v 'x/${u}/priv:${u}/priv:A,${u}'             # only-user-admin

now it realizes that this is safe because both IdP volumes
will be created/owned by the same user

however, if the first volume is 'x::r' then this is NOT safe,
and is now still correctly detected as being dangerous

also add a separate warning if `${g}` and `${u}` is mixed
in a volpath, since that is PROBABLY (not provably) unsafe
2025-03-14 21:08:21 +00:00
ed 815d46f2c4 this keyboard sure is bouncy 2025-03-09 21:14:53 +01:00
ed 8417098c68 fix dl from shares with -j0; closes #146
`write_dls` assumed `vfs.all_nodes` included shares; make it so

shares now also appear in the active-downloads list, but the
URL is hidden unless the viewer definitely already knows the
share exists (which is why vfs-nodes now have `shr_owner`)

also adds PRTY_FORCE_MP, a beefybit (opposite of chickenbit)
to allow multiprocessing on known-buggy platforms (macos)
2025-03-09 21:10:31 +01:00
ed 25974d660d improve errmsg when reading non-utf8 files (#143)
previously, the native python-error was printed when reading
the contents of a textfile using the wrong character encoding

while technically correct, it could be confusing for end-users

add a helper to produce a more helpful errormessage when
someone (for example) tries to load a latin-1 config file
2025-03-09 11:59:33 +01:00
ed 12fcb42201 github: mention preferred language 2025-03-08 23:58:07 +02:00
ed 16462ee573 xff-warning: suggest proper /64 for ipv6 2025-03-06 19:57:20 +01:00
ed 540664e0c2 usb-eject nitpicks
* fix navpane reload
* strip trailing newlines in toasts
2025-03-02 23:51:35 +00:00
ed b5cb763ab1 usb-eject: treepar fix
since this dumb plugin found an actual usecase,
fix the most glaring issue

when nodes overflow from treeul into treepar, the
eject-button is cloned over as well, but the clone
does nothing (as expected), though this will also
cause a flood of new eject-buttons appearing, and
that's worth fixing

NB: check treeul + treepar explicitly; avoid docul
2025-03-01 22:03:18 +00:00
ed c24a0ec364 update pkgs to 1.16.16 2025-02-28 19:16:27 +00:00
ed 4accef00fb v1.16.16 2025-02-28 18:46:32 +00:00
ed d779525500 move -volflag warning to avoid false positives 2025-02-28 18:13:23 +00:00
ed 65a7706f77 add helptext for volflags dk, dks, dky 2025-02-28 17:56:51 +00:00
ed 5e12abbb9b ignore impossible lastmod on upload; closes #142
android-chrome bug https://issues.chromium.org/issues/393149335
sends last-modified time `-11644473600` for all uploads

this has been fixed in chromium, but there might be similar
bugs in other browsers, so add server-side and client-side
detection for unreasonable lastmod times

previously, if the js detected a similar situation, it would
substitute the lastmod-time with the client's wallclock, but
now the server's wallclock is always preferrred as fallback
2025-02-28 17:48:14 +00:00
daimond113 e0fe2b97be nix: add mainProgram
Silences warnings like "getExe: Package
"copyparty-1.16.15" does not have the
meta.mainProgram attribute. We'll assume that the
main program has the same name for now, but this
behavior is deprecated, because it leads to
surprising errors when the assumption does not
hold. If the package has a main program, please
set `meta.mainProgram` in its definition to make
this warning go away. Otherwise, if the package
does not have a main program, or if you don't
control its definition, use getExe' to specify
the name to the program, such as lib.getExe' foo
"bar"."
2025-02-26 23:07:19 +01:00
ed bd33863f9f update pkgs to 1.16.15 2025-02-25 01:25:15 +00:00
ed a011139894 v1.16.15 2025-02-25 00:17:58 +00:00
ed 36866f1d36 dangit.wav 2025-02-25 00:11:57 +00:00
ed 407531bcb1 fix markdown / text-editor jank
* only indicate file-history for markdown files since
   other files won't load into the editor which makes
   that entirely pointless; do file extension instead

* text-editor: in files containing one single line,
   ^C followed by ^V ^Z would accidentally a letter

and fix unhydrated extensions
2025-02-25 00:03:22 +00:00
ed 3adbb2ff41 https://youtu.be/WyXebd3I3Vo 2025-02-24 23:32:03 +00:00
ed 499ae1c7a1 other minor html-escaping fixes
mostly related to error-handling for uploads, network-loss etc,
nothing worse than the dom-xss just now
2025-02-24 22:42:05 +00:00
ed 438ea6ccb0 fix GHSA-m2jw-cj8v-937r ;
this fixes a DOM-Based XSS when preparing files for upload;
empty files would have their filenames rendered as HTML in
a messagebox, making it possible to trick users into running
arbitrary javascript by giving them maliciously-named files

note that, being a general-purpose webserver, it is still
intentionally possible to upload and execute arbitrary
javascript, just not in this unexpected manner
2025-02-24 21:23:13 +00:00
ed 598a29a733 mention sony psp support (thx dwarf) 2025-02-23 21:37:21 +00:00
ed 6d102fc826 mention risc-v support 2025-02-20 04:51:04 +00:00
ed fca07fbb62 update pkgs to 1.16.14 2025-02-19 23:35:05 +00:00
ed cdedcc24b8 v1.16.14 2025-02-19 23:09:14 +00:00
ed 60d5f27140 new example: randpic.py 2025-02-19 22:41:30 +00:00
ed cb413bae49 webdav: a healthy dash of paranoia
there's probably at least one client sending `Overwrite: False`
instead of the spec-correct `Overwrite: F`
2025-02-19 22:07:26 +00:00
ed e9f78ea70c up2k: tristate option for overwriting files; closes #139
adds a third possible value for the `replace` property in handshakes:

* absent or False: never overwrite an existing file on the server,
   and instead generate a new filename to avoid collision

* True: always overwrite existing files on the server

* "mt": only overwrite if client's last-modified is more recent
   (this is the new option)

the new UI button toggles between all three options,
defaulting to never-overwrite
2025-02-19 21:58:56 +00:00
ed 6858cb066f spinner: themes + improve positioning
loading-spinner is either `#dlt_t` or `#dlt_f`
(tree or files), appearing top-left or top-right,
regardless of page/tree scroll (position:fixed)
2025-02-19 18:55:33 +00:00
ed 4be0d426f4 option to forget uploader-IP from db after some time
does this mean copyparty is GDPR-compliant now? idklol
2025-02-17 23:47:59 +00:00
ed 7d7d5d6c3c fix custom spinner css on initial page load 2025-02-17 23:26:21 +00:00
ed 0422387e90 readme: changing the loading spinner (#138) 2025-02-16 19:28:57 +00:00
ed 2ed5fd9ac4 readme: diagnosing broken thumbnails (#137) 2025-02-16 19:22:17 +00:00
ed 2beb2acc24 readme: permanent cloudflare tunnel (#137) 2025-02-16 18:59:18 +00:00
ed 56ce591908 synology dsm: add updating 2025-02-16 18:12:35 +00:00
ed b190e676b4 fix cosmetic volflag stuff:
* `xz` would show the "unrecognized volflag" warning,
   but it still applied correctly

* removing volflags with `-foo` would also show the warning
   but it would still get removed correctly

* hide `ext_th_d` in the startup volume-listing
2025-02-14 20:54:13 +00:00
ed 19520b2ec9 remove patch for musl cve (no longer necessary) 2025-02-14 09:15:52 +00:00
ed eeb96ae8b5 update pkgs to 1.16.13 2025-02-13 21:43:32 +00:00
ed cddedd37d5 v1.16.13 2025-02-13 20:57:04 +00:00
ed 4d6626b099 workaround musl 1.2.5 cve 2025-02-13 20:53:47 +00:00
ed 7a55833bb2 silence linter 2025-02-13 18:34:41 +00:00
ed 7e4702cf09 file-extension icons
global-option / volflag `ext_th` specifies
custom thumbnail for a given file extension
2025-02-13 18:32:01 +00:00
ed 685f08697a alternative loader spinners 2025-02-13 17:07:48 +00:00
ed a255db706d make volflags less confusing
1. warn about unrecognized volflags

previously, when specifying an unknown volflag, it would
be silently ignored, giving the impression that it applied

2. also allow uppercase, kebab-case
    (previously, only snake_case was accepted)

3. mention every volflag in --help-flags
    (some volflags were missing)
2025-02-13 00:34:46 +00:00
ed 9d76902710 WebDAV: adjust 401-mask for GETs (#136)
some clients, including KDE Dolphin (kioworker/6.10) keeps
sending requests without the basic-auth header, expecting
the server to respond with a 401 before it does

most clients only do this for the initial request, which is
usually a PROPFIND, which makes this nice and simple -- but
turns out we need to consider this for GET as well...

this is tricky because a graphical webbrowser must never
receive a 401 lest it becomes near-impossible to deauth,
and that's exactly what Dolphin pretends to be in its UA

man ( ´_ゝ`)

note: `KIO/` hits konqueror so don't
2025-02-11 23:32:44 +00:00
ed 62ee7f6980 WebDAV: support COPY, KDE-Dolphin (#136):
* add support for the COPY verb

* COPY/MOVE: add overwrite support;
   default is True according to rfc
   (only applies to single files for now)

* COPY/MOVE/MKCOL: return 401 as necessary
   for clients which rechallenge frequently
   such as KDE Dolphin (KIO/6.10)

* MOVE: support webdav:// Destination prefix
   as used by KDE Dolphin (KIO/6.10)

* MOVE: vproxy support
2025-02-11 21:34:24 +00:00
ed 2f6707825a improve usb-eject hook:
* don't crash internet explorer
* support running as root
* support old linuxen
2025-02-10 04:52:57 +00:00
ed 7dda77dcb4 update pkgs to 1.16.12 2025-02-10 00:04:04 +00:00
147 changed files with 20858 additions and 1801 deletions

View file

@ -8,6 +8,7 @@ assignees: '9001'
---
NOTE:
**please use english, or include an english translation.** aside from that,
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
@ -33,7 +34,7 @@ remove the ones that are not relevant:
### Server details (if you're NOT using docker/podman)
remove the ones that are not relevant:
* **server OS / version:**
* **what copyparty did you grab:** (sfx/exe/pip/aur/...)
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
* **how you're running it:** (in a terminal, as a systemd-service, ...)
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
* **copyparty arguments and/or config-file:**

View file

@ -7,6 +7,8 @@ assignees: '9001'
---
NOTE:
**please use english, or include an english translation.** aside from that,
all of the below are optional, consider them as inspiration, delete and rewrite at will
**is your feature request related to a problem? Please describe.**

3
.gitignore vendored
View file

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

View file

@ -1,8 +1,21 @@
* do something cool
* **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :>
* **fixed a bug?** create a PR or post a patch! big thx in advance :>
* **have a cool idea?** let's discuss it! anywhere's fine, you choose.
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight 👍👍
but please:
# do not use AI / LMM when writing code
copyparty is 100% organic, free-range, human-written software!
> ⚠ you are now entering a no-copilot zone
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
sorry for the harsh tone, but this is important to me 🙏
but to be more specific,
# contribution ideas

315
README.md
View file

@ -8,11 +8,13 @@ turn almost any device into a file server with resumable uploads/downloads using
* 🔌 protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running on a nuc in my basement
📷 **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 🇳🇴
## readme toc
@ -50,8 +52,11 @@ turn almost any device into a file server with resumable uploads/downloads using
* [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
* [recent uploads](#recent-uploads) - list all recent uploads
* [media player](#media-player) - plays almost every audio format there is
* [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists
* [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
* [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/))
* [markdown viewer](#markdown-viewer) - and there are *two* editors
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
* [other tricks](#other-tricks)
@ -75,6 +80,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [periodic rescan](#periodic-rescan) - filesystem monitoring
* [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
* [other flags](#other-flags)
* [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
@ -84,7 +90,9 @@ turn almost any device into a file server with resumable uploads/downloads using
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
* [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
* [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
* [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
@ -94,12 +102,15 @@ turn almost any device into a file server with resumable uploads/downloads using
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
* [real-ip](#real-ip) - teaching copyparty how to see client IPs
* [reverse-proxy performance](#reverse-proxy-performance)
* [permanent cloudflare tunnel](#permanent-cloudflare-tunnel) - if you have a domain and want to get your copyparty online real quick
* [prometheus](#prometheus) - metrics/stats can be enabled
* [other extremely specific features](#other-extremely-specific-features) - you'll never find a use for these
* [custom mimetypes](#custom-mimetypes) - change the association of a file extension
* [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...
* [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
* [feature beefybits](#feature-beefybits) - force-enable features with known issues on your OS/env
* [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
* [fedora package](#fedora-package) - does not exist yet
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module)
@ -138,12 +149,16 @@ turn almost any device into a file server with resumable uploads/downloads using
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 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 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 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 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...
* ...maybe [prisonparty](./bin/prisonparty.sh) to create a tiny [chroot](https://wiki.archlinux.org/title/Chroot) (very portable),
* ...or [bubbleparty](./bin/bubbleparty.sh) to wrap it in [bubblewrap](https://github.com/containers/bubblewrap) (much better)
@ -159,8 +174,8 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
* **MacOS:** `port install py-Pillow ffmpeg`
* **MacOS** (alternative): `brew install pillow ffmpeg`
* **Windows:** `python -m pip install --user -U Pillow`
* install python and ffmpeg manually; do not use `winget` or `Microsoft Store` (it breaks $PATH)
* copyparty.exe comes with `Pillow` and only needs `ffmpeg`
* install [python](https://www.python.org/downloads/windows/) and [ffmpeg](#optional-dependencies) manually; do not use `winget` or `Microsoft Store` (it breaks $PATH)
* copyparty.exe comes with `Pillow` and only needs [ffmpeg](#optional-dependencies) for mediatags/videothumbs
* see [optional dependencies](#optional-dependencies) to enable even more features
running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
@ -183,6 +198,8 @@ first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/co
as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you
but if you have a domain, then you probably want to skip the random autogenerated URL and instead make a [permanent cloudflare tunnel](#permanent-cloudflare-tunnel)
since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
@ -224,6 +241,7 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)
* ☑ [event hooks](#event-hooks) / script runner
* ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)
* ☑ cross-platform (Windows, Linux, Macos, Android, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
* upload
* ☑ basic: plain multipart, ie6 support
* ☑ [up2k](#uploading): js, resumable, multithreaded
@ -244,10 +262,13 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
* ☑ audio player (with [OS media controls](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) and opus/mp3 transcoding)
* ☑ play video files as audio (converted on server)
* ☑ create and play [m3u8 playlists](#playlists)
* ☑ image gallery with webm player
* ☑ textfile browser with syntax hilighting
* ☑ [textfile browser](#textfile-viewer) with syntax hilighting
* ☑ realtime streaming of growing files (logfiles and such)
* ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow, pyvips, or FFmpeg
* ☑ ...of RAW images using rawpy
* ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually)
@ -275,6 +296,8 @@ small collection of user feedback
`good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`, `wow this is better than nextcloud`
* UI просто ужасно. Если буду описывать детально не смогу удержаться в рамках приличий
# motivations
@ -294,6 +317,8 @@ project goals / philosophy
* adaptable, malleable, hackable
* no build steps; modify the js/python without needing node.js or anything like that
becoming rich is specifically *not* a motivation, but if you wanna donate then see my [github profile](https://github.com/9001) regarding donations for my FOSS stuff in general (also THANKS!)
## notes
@ -323,7 +348,8 @@ roughly sorted by chance of encounter
* `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions (macos, some linux)
* `--th-ff-swr` may fix audio thumbnails on some FFmpeg versions
* if the `up2k.db` (filesystem index) is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db on a local disk instead
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db and thumbnails on a local disk instead
* or, if you only want to move the db (and not the thumbnails), then use `--dbpath` or the `dbpath` volflag
* all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise
* probably more, pls let me know
@ -376,7 +402,8 @@ same order here too
* this is an msys2 bug, the regular windows edition of python is fine
* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db inside the vm instead
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db and thumbnails inside the vm instead
* or, if you only want to move the db (and not the thumbnails), then use `--dbpath` or the `dbpath` volflag
* also happens on mergerfs, so put the db elsewhere
* Ubuntu: dragging files from certain folders into firefox or chrome is impossible
@ -401,12 +428,19 @@ upgrade notes
"frequently" asked questions
* CopyParty?
* nope! the name is either copyparty (all-lowercase) or Copyparty -- it's [one word](https://en.wiktionary.org/wiki/copyparty) after all :>
* can I change the 🌲 spinning pine-tree loading animation?
* [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-(
* is it possible to block read-access to folders unless you know the exact URL for a particular file inside?
* yes, using the [`g` permission](#accounts-and-volumes), see the examples there
* you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty
* 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
* if you have enabled `--usernames` then do `?pw=username:password` instead
* 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)
@ -423,6 +457,14 @@ upgrade notes
* copyparty seems to think I am using http, even though the URL is https
* your reverse-proxy is not sending the `X-Forwarded-Proto: https` header; this could be because your reverse-proxy itself is confused. Ensure that none of the intermediates (such as cloudflare) are terminating https before the traffic hits your entrypoint
* thumbnails are broken (you get a colorful square which says the filetype instead)
* you need to install `FFmpeg` or `Pillow`; see [thumbnails](#thumbnails)
* thumbnails are broken (some images appear, but other files just get a blank box, and/or the broken-image placeholder)
* probably due to a reverse-proxy messing with the request URLs and stripping the query parameters (`?th=w`), so check your URL rewrite rules
* could also be due to incorrect caching settings in reverse-proxies and/or CDNs, so make sure that nothing is set to ignore the query string
* could also be due to misbehaving privacy-related browser extensions, so try to disable those
* i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion
* ```bash
_| _ __ _ _|_
@ -473,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, 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
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
[accounts]
u1: p1 # create account "u1" with password "p1"
u2: p2 # (note that comments must have
u3: p3 # two spaces before the # sign)
[groups]
g1: u1, u2 # create a group
[/] # this URL will be mapped to...
/srv # ...this folder on the server filesystem
accs:
@ -492,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
accs:
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
[/inc]
@ -529,6 +580,8 @@ a client can request to see dotfiles in directory listings if global option `-ed
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
> even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab
config file example, where the same permission to see dotfiles is given in two different ways just for reference:
```yaml
@ -653,6 +706,7 @@ press `g` or `田` to toggle grid-view instead of the file listing and `t` togg
it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg
* disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only
* for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies)
audio files are converted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
@ -664,6 +718,11 @@ enabling `multiselect` lets you click files to select them, and then shift-click
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
* the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`
to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` and `.elf` files respectively, do this: `--ext-th=exe=/icons/exe.png --ext-th=elf=/icons/elf.gif`
* optionally as separate volflags for each mapping; see config file example below
* the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)
* be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however
config file example:
```yaml
@ -678,6 +737,8 @@ config file example:
flags:
dthumb # disable ALL thumbnails and audio transcoding
dvthumb # only disable video thumbnails
ext-th: exe=/ico/exe.png # /ico/exe.png is the thumbnail of *.exe
ext-th: elf=/ico/elf.gif # ...and /ico/elf.gif is used for *.elf
th-covers: folder.png,folder.jpg,cover.png,cover.jpg # the default
```
@ -705,6 +766,7 @@ select which type of archive you want in the `[⚙️] config` tab:
* `up2k.db` and `dir.txt` is always excluded
* bsdtar supports streaming unzipping: `curl foo?zip | bsdtar -xv`
* good, because copyparty's zip is faster than tar on small files
* but `?tar` is better for large files, especially if the total exceeds 4 GiB
* `zip_crc` will take longer to download since the server has to read each file twice
* this is only to support MS-DOS PKZIP v2.04g (october 1993) and older
* how are you accessing copyparty actually
@ -761,8 +823,11 @@ the up2k UI is the epitome of polished intuitive experiences:
* "parallel uploads" specifies how many chunks to upload at the same time
* `[🏃]` analysis of other files should continue while one is uploading
* `[🥔]` shows a simpler UI for faster uploads from slow devices
* `[🛡️]` decides when to overwrite existing files on the server
* `🛡️` = never (generate a new filename instead)
* `🕒` = overwrite if the server-file is older
* `♻️` = always overwrite if the files are different
* `[🎲]` generate random filenames during upload
* `[📅]` preserve last-modified timestamps; server times will match yours
* `[🔎]` switch between upload and [file-search](#file-search) mode
* ignore `[🔎]` if you add files by dragging them into the browser
@ -780,6 +845,8 @@ if you are resuming a massive upload and want to skip hashing the files which al
if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option `--u2sz` (the default is 96 MiB to support Cloudflare)
if you want to replace existing files on the server with new uploads by default, run with `--u2ow 2` (only works if users have the delete-permission, and can still be disabled with `🛡️` in the UI)
### file-search
@ -878,6 +945,7 @@ semi-intentional limitations:
* cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d`
* only folders from the same volume are shared; if you are sharing a folder which contains other volumes, then the contents of those volumes will not be available
* if you change [password hashing](#password-hashing) settings after creating a password-protected share, then that share will stop working
* related to [IdP volumes being forgotten on shutdown](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#idp-volumes-are-forgotten-on-shutdown), any shares pointing into a user's IdP volume will be unavailable until that user makes their first request after a restart
* no option to "delete after first access" because tricky
* when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
@ -955,6 +1023,7 @@ a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3
url parameters:
* `pw=hunter2` for password auth
* if you enabled `--usernames` then do `pw=username:password` instead
* `recursive` to also include subfolders
* `title=foo` changes the feed title (default: folder name)
* `fext=mp3,opus` only include mp3 and opus files (default: all)
@ -1004,11 +1073,13 @@ click the `play` link next to an audio file, or copy the link target to [share i
open the `[🎺]` media-player-settings tab to configure it,
* "switches":
* `[🔁]` repeats one single song forever
* `[🔀]` shuffles the files inside each folder
* `[preload]` starts loading the next track when it's about to end, reduces the silence between songs
* `[full]` does a full preload by downloading the entire next file; good for unreliable connections, bad for slow connections
* `[~s]` toggles the seekbar waveform display
* `[/np]` enables buttons to copy the now-playing info as an irc message
* `[📻]` enables buttons to create an [m3u playlist](#playlists) with the selected songs
* `[os-ctl]` makes it possible to control audio playback from the lockscreen of your device (enables [mediasession](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession))
* `[seek]` allows seeking with lockscreen controls (buggy on some devices)
* `[art]` shows album art on the lockscreen
@ -1027,11 +1098,42 @@ open the `[🎺]` media-player-settings tab to configure it,
* "transcode to":
* `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs)
* `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1
* `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the mos tpart
* `[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
* `[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
### playlists
create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists -- see example [text](https://a.ocv.me/pub/demo/music/?doc=example-playlist.m3u) and [player](https://a.ocv.me/pub/demo/music/#m3u=example-playlist.m3u)
click a file with the extension `m3u` or `m3u8` (for example `mixtape.m3u` or `touhou.m3u8` ) and you get two choices: Play / Edit
playlists can include songs across folders anywhere on the server, but filekeys/dirkeys are NOT supported, so the listener must have read-access or get-access to the files
### creating a playlist
with a standalone mediaplayer or copyparty
you can use foobar2000, deadbeef, just about any standalone player should work -- but you might need to edit the filepaths in the playlist so they fit with the server-URLs
alternatively, you can create the playlist using copyparty itself:
* open the `[🎺]` media-player-settings tab and enable the `[📻]` create-playlist feature -- this adds two new buttons in the bottom-right tray, `[📻add]` and `[📻copy]` which appear when you listen to music, or when you select a few audiofiles
* click the `📻add` button while a song is playing (or when you've selected some songs) and they'll be added to "the list" (you can't see it yet)
* at any time, click `📻copy` to send the playlist to your clipboard
* you can then continue adding more songs if you'd like
* if you want to wipe the playlist and start from scratch, just refresh the page
* create a new textfile, name it `something.m3u` and paste the playlist there
### audio equalizer
and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
@ -1048,6 +1150,18 @@ not available on iPhones / iPads because AudioContext currently breaks backgroun
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
## textfile viewer
with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/)) , and terminal colors work too
click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:
* `✏️ edit` opens the textfile editor
* `📡 follow` starts monitoring the file for changes, streaming new lines in realtime
* similar to `tail -f`
* [link directly](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail) to a file with tailing enabled by adding `&tail` to the textviewer URL
## markdown viewer
and there are *two* editors
@ -1123,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.
* 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
@ -1195,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
* 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
* unless you enabled `--usernames`
some recommended FTP / FTPS clients; `wark` = example password:
* https://winscp.net/eng/download.php
@ -1212,6 +1327,7 @@ click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to se
general usage:
* 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:
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
@ -1227,6 +1343,7 @@ using the GUI (winXP or later):
* 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
* 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:
* 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
@ -1284,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)
* 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
* not compatible with pw-hashing or `--usernames`
and some minor issues,
* clients only see the first ~400 files in big folders;
@ -1330,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
> 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)
@ -1346,12 +1466,17 @@ if you enable deduplication with `--dedup` then it'll create a symlink instead o
**warning:** when enabling dedup, you should also:
* 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 `--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)
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:
* hardlinks are more compatible with other software; they behave entirely like regular files
@ -1408,7 +1533,6 @@ the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`,
note:
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
config file example (these options are recommended btw):
@ -1513,7 +1637,7 @@ config file example:
w: * # anyone can upload here
rw: ed # only user "ed" can read-write
flags:
e2ds: # filesystem indexing is required for many of these:
e2ds # filesystem indexing is required for many of these:
sz: 1k-3m # accept upload only if filesize in this range
df: 4g # free disk space cannot go lower than this
vmaxb: 1g # volume can never exceed 1 GiB
@ -1552,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`
## 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
* `:c,magic` enables filetype detection for nameless uploads, same as `--magic`
@ -1568,6 +1712,10 @@ copyparty creates a subfolder named `.hist` inside each volume where it stores t
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volflag, or a mix of both:
* `--hist ~/.cache/copyparty -v ~/music::r:c,hist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
by default, the per-volume `up2k.db` sqlite3-database for `-e2d` and `-e2t` is stored next to the thumbnails according to the `--hist` option, but the global-option `--dbpath` and/or volflag `dbpath` can be used to put the database somewhere else
if your storage backend is unreliable (NFS or bad HDDs), you can specify one or more "landmarks" to look for before doing anything database-related. A landmark is a file which is always expected to exist inside the volume. This avoids spurious filesystem rescans in the event of an outage. One line per landmark (see example below)
note:
* putting the hist-folders on an SSD is strongly recommended for performance
* markdown edits are always stored in a local `.hist` subdirectory
@ -1585,6 +1733,8 @@ config file example:
flags:
hist: - # restore the default (/mnt/nas/pics/.hist/)
hist: /mnt/nas/cache/pics/ # can be absolute path
landmark: me.jpg # /mnt/nas/pics/me.jpg must be readable to enable db
landmark: info/a.txt^=ok # and this textfile must start with "ok"
```
@ -1748,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=`
### 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
replace copyparty passwords with oauth and such
@ -1756,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
* 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)
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)
@ -1765,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
### 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
if permitted, users can change their own passwords in the control-panel
@ -1813,7 +1993,7 @@ tell search engines you don't wanna be indexed, either using the good old [robo
* volflag `[...]:c,norobots` does the same thing for that single volume
* volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for *some* search engines -- note that crawlers which understand javascript (such as google) will not be affected
## themes
@ -1918,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
* 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
* **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
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:
@ -1979,6 +2163,26 @@ in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds
* if these results are bullshit because my config exampels are bad, please submit corrections!
## permanent cloudflare tunnel
if you have a domain and want to get your copyparty online real quick, either from your home-PC behind a CGNAT or from a server without an existing [reverse-proxy](#reverse-proxy) setup, one approach is to create a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) (formerly "Argo Tunnel")
I'd recommend making a `Locally-managed tunnel` for more control, but if you prefer to make a `Remotely-managed tunnel` then this is currently how:
* `cloudflare dashboard` » `zero trust` » `networks` » `tunnels` » `create a tunnel` » `cloudflared` » choose a cool `subdomain` and leave the `path` blank, and use `service type` = `http` and `URL` = `127.0.0.1:3923`
* and if you want to just run the tunnel without installing it, skip the `cloudflared service install BASE64` step and instead do `cloudflared --no-autoupdate tunnel run --token BASE64`
NOTE: since people will be connecting through cloudflare, as mentioned in [real-ip](#real-ip) you should run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
config file example:
```yaml
[global]
xff-hdr: cf-connecting-ip
```
## prometheus
metrics/stats can be enabled at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0)
@ -2065,13 +2269,27 @@ in a config file, this is the same as:
run copyparty with `--mimes` to list all the default mappings
### GDPR compliance
imagine using copyparty professionally... **TINLA/IANAL; EU laws are hella confusing**
* remember to disable logging, or configure logrotation to an acceptable timeframe with `-lo cpp-%Y-%m%d.txt.xz` or similar
* if running with the database enabled (recommended), then have it forget uploader-IPs after some time using `--forget-ip 43200`
* don't set it too low; [unposting](#unpost) a file is no longer possible after this takes effect
* if you actually *are* a lawyer then I'm open for feedback, would be fun
### feature chickenbits
buggy feature? rip it out by setting any of the following environment variables to disable its associated bell or whistle,
| env-var | what it does |
| -------------------- | ------------ |
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
| `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads |
| `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |
@ -2082,6 +2300,16 @@ buggy feature? rip it out by setting any of the following environment variables
example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
### feature beefybits
force-enable features with known issues on your OS/env by setting any of the following environment variables, also affectionately known as `fuckitbits` or `hail-mary-bits`
| env-var | what it does |
| ------------------------ | ------------ |
| `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
the party might be closer than you think
@ -2091,14 +2319,16 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
## arch package
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
`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, 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)
## 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
@ -2223,8 +2453,10 @@ TLDR: yes
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| file search | - | yep | yep | yep | yep | yep | yep | yep |
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
| unpost uploads | - | - | yep | yep | yep | yep | yep | yep |
| navpane | - | yep | yep | yep | yep | yep | yep | yep |
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
| video player | - | yep | yep | yep | yep | yep | yep | yep |
@ -2252,7 +2484,9 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
| **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) |
| **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) |
| **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>
@ -2296,6 +2530,9 @@ interact with copyparty using non-browser clients
* and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)
* and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
* [Custom Uploader](https://f-droid.org/en/packages/com.nyx.custom_uploader/) (an Android app) as an alternative to copyparty's own [PartyUP!](#android-app)
* works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f`
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`
@ -2307,6 +2544,10 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
you can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, url-param `?pw=hunter2`, or with basic-authentication (either as the username or password)
> 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
@ -2379,6 +2620,8 @@ below are some tweaks roughly ordered by usefulness:
* `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable
* if your volumes are on a network-disk such as NFS / SMB / s3, specifying larger values for `--iobuf` and/or `--s-rd-sz` and/or `--s-wr-sz` may help; try setting all of them to `524288` or `1048576` or `4194304`
* `--no-htp --hash-mt=0 --mtag-mt=1 --th-mt=1` minimizes the number of threads; can help in some eccentric environments (like the vscode debugger)
* when running on AlpineLinux or other musl-based distro, try mimalloc for higher performance (and twice as much RAM usage); `apk add mimalloc2` and run copyparty with env-var `LD_PRELOAD=/usr/lib/libmimalloc-secure.so.2`
* note that mimalloc requires special care when combined with prisonparty and/or bubbleparty/bubblewrap; you must give it access to `/proc` and `/sys` otherwise you'll encounter issues with FFmpeg (audio transcoding, thumbnails)
* `-j0` enables multiprocessing (actual multithreading), can reduce latency to `20+80/numCores` percent and generally improve performance in cpu-intensive workloads, for example:
* lots of connections (many users or heavy clients)
* simultaneous downloads and uploads saturating a 20gbps connection
@ -2393,6 +2636,11 @@ below are some tweaks roughly ordered by usefulness:
when uploading files,
* when uploading from very fast storage (NVMe SSD) with chrome/firefox, enable `[wasm]` in the `[⚙️] settings` tab to more effectively use all CPU-cores for hashing
* don't do this on Safari (runs faster without)
* don't do this on older browsers; likely to provoke browser-bugs (browser eats all RAM and crashes)
* can be made default-enabled serverside with `--nosubtle 137` (chrome v137+) or `--nosubtle 2` (chrome+firefox)
* chrome is recommended (unfortunately), at least compared to firefox:
* up to 90% faster when hashing, especially on SSDs
* up to 40% faster when uploading over extremely fast internets
@ -2411,7 +2659,7 @@ there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone`
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
* 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
@ -2511,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
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
@ -2572,9 +2826,10 @@ enable [music tags](#metadata-from-audio-files):
enable [thumbnails](#thumbnails) of...
* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler)
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin`
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pillow-heif`
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
* **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`
@ -2582,6 +2837,8 @@ enable [smb](#smb-server) support (**not** recommended): `impacket==0.12.0`
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
to install FFmpeg on Windows, grab [a recent build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) -- you need `ffmpeg.exe` and `ffprobe.exe` from inside the `bin` folder; copy them into `C:\Windows\System32` or any other folder that's in your `%PATH%`
### dependency chickenbits
@ -2598,13 +2855,15 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
| `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |
| `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |
| `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_PIL_AVIF` | disable 3rd-party Pillow plugin for [AVIF support](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_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/pillow-heif/) |
| `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_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 |
example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
@ -2625,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
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)
@ -2652,7 +2913,7 @@ then again, if you are already into downloading shady binaries from the internet
## 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
@ -2698,5 +2959,7 @@ if there's a wall of base64 in the log (thread stacks) then please include that,
for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
specifically you may want to [build the sfx](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#just-the-sfx) or [build from scratch](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch)
see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes

View file

@ -21,6 +21,7 @@ each plugin must define a `main()` which takes 3 arguments;
## on404
* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file
* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary

35
bin/handlers/randpic.py Normal file
View file

@ -0,0 +1,35 @@
import os
import random
from urllib.parse import quote
# assuming /foo/bar/ is a valid URL but /foo/bar/randpic.png does not exist,
# hijack the 404 with a redirect to a random pic in that folder
#
# thx to lia & kipu for the idea
def main(cli, vn, rem):
req_fn = rem.split("/")[-1]
if not cli.can_read or not req_fn.startswith("randpic"):
return
req_abspath = vn.canonical(rem)
req_ap_dir = os.path.dirname(req_abspath)
files_in_dir = os.listdir(req_ap_dir)
if "." in req_fn:
file_ext = "." + req_fn.split(".")[-1]
files_in_dir = [x for x in files_in_dir if x.lower().endswith(file_ext)]
if not files_in_dir:
return
selected_file = random.choice(files_in_dir)
req_url = "/".join([vn.vpath, rem]).strip("/")
req_dir = req_url.rsplit("/", 1)[0]
new_url = "/".join([req_dir, quote(selected_file)]).strip("/")
cli.reply(b"redirecting...", 302, headers={"Location": "/" + new_url})
return "true"

View file

@ -14,6 +14,8 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xm
* [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png))
* [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable
* [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded
* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded
* good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index
# upload batches
@ -25,6 +27,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
# before upload
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
# on message

121
bin/hooks/podcast-normalizer.py Executable file
View file

@ -0,0 +1,121 @@
#!/usr/bin/env python3
import json
import os
import sys
import subprocess as sp
_ = r"""
sends all uploaded audio files through an aggressive
dynamic-range-compressor to even out the volume levels
dependencies:
ffmpeg
being an xau hook, this gets eXecuted After Upload completion
but before copyparty has started hashing/indexing the file, so
we'll create a second normalized copy in a subfolder and tell
copyparty to hash/index that additional file as well
example usage as global config:
-e2d -e2t --xau j,c1,bin/hooks/podcast-normalizer.py
parameters explained,
e2d/e2t = enable database and metadata indexing
xau = execute after upload
j = this hook needs upload information as json (not just the filename)
c1 = this hook returns json on stdout, so tell copyparty to read that
example usage as a volflag (per-volume config):
-v srv/inc/pods:inc/pods:r:rw,ed:c,xau=j,c1,bin/hooks/podcast-normalizer.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share fs-path srv/inc/pods at URL /inc/pods,
readable by all, read-write for user ed,
running this xau (exec-after-upload) plugin for all uploaded files)
example usage as a volflag in a copyparty config file:
[/inc/pods]
srv/inc/pods
accs:
r: *
rw: ed
flags:
e2d # enables file indexing
e2t # metadata tags too
xau: j,c1,bin/hooks/podcast-normalizer.py
"""
########################################################################
### CONFIG
# filetypes to process; ignores everything else
EXTS = "mp3 flac ogg oga opus m4a aac wav wma"
# the name of the subdir to put the normalized files in
SUBDIR = "normalized"
########################################################################
# try to enable support for crazy filenames
try:
from copyparty.util import fsenc
except:
def fsenc(p):
return p.encode("utf-8")
def main():
# read info from copyparty
inf = json.loads(sys.argv[1])
vpath = inf["vp"]
abspath = inf["ap"]
# check if the file-extension is on the to-be-processed list
ext = abspath.lower().split(".")[-1]
if ext not in EXTS.split():
return
# jump into the folder where the file was uploaded
# and create the subfolder to place the normalized copy inside
dirpath, filename = os.path.split(abspath)
os.chdir(fsenc(dirpath))
os.makedirs(SUBDIR, exist_ok=True)
# the input and output filenames to give ffmpeg
fname_in = fsenc(f"./{filename}")
fname_out = fsenc(f"{SUBDIR}/{filename}.opus")
# fmt: off
# create and run the ffmpeg command
cmd = [
b"ffmpeg",
b"-nostdin",
b"-hide_banner",
b"-i", fname_in,
b"-af", b"dynaudnorm=f=100:g=9", # the normalizer config
b"-c:a", b"libopus",
b"-b:a", b"128k",
fname_out,
]
# fmt: on
sp.check_output(cmd)
# and finally, tell copyparty about the new file
# so it appears in the database and rss-feed:
vpath = f"{SUBDIR}/{filename}.opus"
print(json.dumps({"idx": {"vp": [vpath]}}))
# (it's fine to give it a relative path like that; it gets
# resolved relative to the folder the file was uploaded into)
if __name__ == "__main__":
try:
main()
except Exception as ex:
print("podcast-normalizer failed; %r" % (ex,))

View file

@ -71,6 +71,9 @@ def main():
## selecting it inside the print at the end:
##
# move all uploads to one specific folder
into_junk = {"vp": "/junk"}
# create a subfolder named after the filetype and move it into there
into_subfolder = {"vp": ext}
@ -92,8 +95,8 @@ def main():
by_category = {} # no action
# now choose the default effect to apply; can be any of these:
# into_subfolder into_toplevel into_sibling by_category
effect = {"vp": "/junk"}
# into_junk into_subfolder into_toplevel into_sibling by_category
effect = into_sibling
##
## but we can keep going, adding more speicifc rules

View file

@ -1,15 +1,17 @@
// see usb-eject.py for usage
function usbclick() {
QS('#treeul a[href="/usb/"]').click();
var o = QS('#treeul a[dst="/usb/"]') || QS('#treepar a[dst="/usb/"]');
if (o)
o.click();
}
function eject_cb() {
var t = this.responseText;
var t = ('' + this.responseText).trim();
if (t.indexOf('can be safely unplugged') < 0 && t.indexOf('Device can be removed') < 0)
return toast.err(30, 'usb eject failed:\n\n' + t);
toast.ok(5, esc(t.replace(/ - /g, '\n\n')));
toast.ok(5, esc(t.replace(/ - /g, '\n\n')).trim());
usbclick(); setTimeout(usbclick, 10);
};
@ -21,10 +23,15 @@ function add_eject_2(a) {
var v = aw[2],
k = 'umount_' + v;
qsr('#' + k);
a.appendChild(mknod('span', k, '⏏'), a);
for (var b = 0; b < 9; b++) {
var o = ebi(k);
if (!o)
break;
o.parentNode.removeChild(o);
}
a.appendChild(mknod('span', k, '⏏'), a);
o = ebi(k);
o.style.cssText = 'position:absolute; right:1em; margin-top:-.2em; font-size:1.3em';
o.onclick = function (e) {
ev(e);
@ -38,8 +45,9 @@ function add_eject_2(a) {
};
function add_eject() {
for (var a of QSA('#treeul a[href^="/usb/"]'))
add_eject_2(a);
var o = QSA('#treeul a[href^="/usb/"]') || QSA('#treepar a[href^="/usb/"]');
for (var a = o.length - 1; a > 0; a--)
add_eject_2(o[a]);
};
(function() {

View file

@ -4,6 +4,7 @@ import os
import stat
import subprocess as sp
import sys
from urllib.parse import unquote_to_bytes as unquote
"""
@ -14,13 +15,13 @@ remove those flashdrives, then boy howdy are you in the right place :D
put usb-eject.js in the webroot (or somewhere else http-accessible)
then run copyparty with these args:
-v /run/media/ed:/usb:A:c,hist=/tmp/junk
-v /run/media/egon:/usb:A:c,hist=/tmp/junk
--xm=c1,bin/hooks/usb-eject.py
--js-browser=/usb-eject.js
which does the following respectively,
* share all of /run/media/ed as /usb with admin for everyone
* share all of /run/media/egon as /usb with admin for everyone
and put the histpath somewhere it won't cause trouble
* run the usb-eject hook with stdout redirect to the web-ui
* add the complementary usb-eject.js to the browser
@ -28,18 +29,30 @@ which does the following respectively,
"""
MOUNT_BASE = b"/run/media/egon/"
def main():
try:
label = sys.argv[1].split(":usb-eject:")[1].split(":")[0]
mp = "/run/media/ed/" + label
mp = MOUNT_BASE + unquote(label)
# print("ejecting [%s]... " % (mp,), end="")
mp = os.path.abspath(os.path.realpath(mp.encode("utf-8")))
mp = os.path.abspath(os.path.realpath(mp))
st = os.lstat(mp)
if not stat.S_ISDIR(st.st_mode):
if not stat.S_ISDIR(st.st_mode) or not mp.startswith(MOUNT_BASE):
raise Exception("not a regular directory")
cmd = [b"gio", b"mount", b"-e", mp]
print(sp.check_output(cmd).decode("utf-8", "replace").strip())
# if you're running copyparty as root (thx for the faith)
# you'll need something like this to make dbus talkative
cmd = b"sudo -u egon DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gio mount -e"
# but if copyparty and the ui-session is running
# as the same user (good) then this is plenty
cmd = b"gio mount -e"
cmd = cmd.split(b" ") + [mp]
ret = sp.check_output(cmd).decode("utf-8", "replace")
print(ret.strip() or (label + " can be safely unplugged"))
except Exception as ex:
print("unmount failed: %r" % (ex,))

View file

@ -2,11 +2,15 @@
import sys
import json
import zlib
import struct
import base64
import hashlib
try:
from zlib_ng import zlib_ng as zlib
except:
import zlib
try:
from copyparty.util import fsenc
except:

View file

@ -22,6 +22,8 @@ set -e
# modifies the keyfinder python lib to load the .so in ~/pe
export FORCE_COLOR=1
linux=1
win=
@ -187,11 +189,14 @@ install_keyfinder() {
exit 1
}
x=${-//[^x]/}; set -x; cat /etc/alpine-release
# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder*
CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
CXXFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \
LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \
PKG_CONFIG_PATH=/c/msys64/mingw64/lib/pkgconfig \
PKG_CONFIG_PATH="/c/msys64/mingw64/lib/pkgconfig:$h/pe/keyfinder/lib/pkgconfig" \
$pybin -m pip install --user keyfinder
[ "$x" ] || set +x
pypath="$($pybin -c 'import keyfinder; print(keyfinder.__file__)')"
for pyso in "${pypath%/*}"/*.so; do

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "2.9"
S_BUILD_DT = "2025-01-27"
S_VERSION = "2.11"
S_BUILD_DT = "2025-05-18"
"""
u2c.py: upload to copyparty
@ -52,6 +52,7 @@ if PY2:
sys.dont_write_bytecode = True
bytes = str
files_decoder = lambda s: unicode(s, "utf8")
else:
from urllib.parse import quote_from_bytes as quote
from urllib.parse import unquote_to_bytes as unquote
@ -61,6 +62,7 @@ else:
from queue import Queue
unicode = str
files_decoder = unicode
WTF8 = "replace" if PY2 else "surrogateescape"
@ -807,7 +809,9 @@ def handshake(ar, file, search):
else:
if ar.touch:
req["umod"] = True
if ar.ow:
if ar.owo:
req["replace"] = "mt"
elif ar.ow:
req["replace"] = True
file.recheck = False
@ -1287,7 +1291,7 @@ class Ctl(object):
if self.ar.jw:
print("%s %s" % (wark, vp))
else:
zd = datetime.datetime.fromtimestamp(file.lmod, UTC)
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year,
zd.month,
@ -1530,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("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("-a", metavar="PASSWD", help="password or $filepath")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
@ -1538,6 +1542,7 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)")
ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming")
ap.add_argument("--owo", action="store_true", help="overwrite existing files if server-file is older")
ap.add_argument("--spd", action="store_true", help="print speeds for each file")
ap.add_argument("--version", action="store_true", help="show version and exit")

View file

@ -50,6 +50,9 @@
* give a 3rd argument to install it to your copyparty config
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
### [`zfs-tune.py`](zfs-tune.py)
* optimizes databases for optimal performance when stored on a zfs filesystem; also see [openzfs docs](https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads) and specifically the SQLite subsection
# OS integration
init-scripts to start copyparty as a service
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally

View file

@ -2,19 +2,38 @@
# not accept more consecutive clients than what copyparty is able to;
# nginx default is 512 (worker_processes 1, worker_connections 512)
#
# ======================================================================
#
# to reverse-proxy a specific path/subpath/location below a domain
# (rather than a complete subdomain), for example "/qw/er", you must
# run copyparty with --rp-loc /qw/as and also change the following:
# location / {
# proxy_pass http://cpp_tcp;
# to this:
# location /qw/er/ {
# proxy_pass http://cpp_tcp/qw/er/;
#
# ======================================================================
#
# rarely, in some extreme usecases, it can be good to add -j0
# (40'000 requests per second, or 20gbps upload/download in parallel)
# but this is usually counterproductive and slightly buggy
#
# ======================================================================
#
# on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1
#
# if you are behind cloudflare (or another protection service),
# ======================================================================
#
# if you are behind cloudflare (or another CDN/WAF/protection service),
# remember to reject all connections which are not coming from your
# protection service -- for cloudflare in particular, you can
# generate the list of permitted IP ranges like so:
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
#
# and then enable it below by uncomenting the cloudflare-only.conf line
#
# ======================================================================
upstream cpp_tcp {
@ -66,13 +85,13 @@ server {
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Host $host;
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 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

@ -1,9 +1,13 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
with lib;
let
mkKeyValue = key: value:
mkKeyValue =
key: value:
if value == true then
# sets with a true boolean value are coerced to just the key name
key
@ -15,9 +19,10 @@ let
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
mkValueString = value:
mkValueString =
value:
if isList value then
(concatStringsSep ", " (map mkValueString value))
(concatStringsSep "," (map mkValueString value))
else if isAttrs value then
"\n" + (mkAttrsString value)
else
@ -49,13 +54,14 @@ let
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
'';
name = "copyparty";
cfg = config.services.copyparty;
configFile = pkgs.writeText "${name}.conf" configStr;
runtimeConfigPath = "/run/${name}/${name}.conf";
home = "/var/lib/${name}";
defaultShareDir = "${home}/data";
in {
configFile = pkgs.writeText "copyparty.conf" configStr;
runtimeConfigPath = "/run/copyparty/copyparty.conf";
externalCacheDir = "/var/cache/copyparty";
externalStateDir = "/var/lib/copyparty";
defaultShareDir = "${externalStateDir}/data";
in
{
options.services.copyparty = {
enable = mkEnableOption "web-based file manager";
@ -68,6 +74,35 @@ in {
'';
};
mkHashWrapper = mkOption {
type = types.bool;
default = true;
description = ''
Make a shell script wrapper called 'copyparty-hash' with all options set here,
that launches the hashing cli.
'';
};
user = mkOption {
type = types.str;
default = "copyparty";
description = ''
The user that copyparty will run under.
If changed from default, you are responsible for making sure the user exists.
'';
};
group = mkOption {
type = types.str;
default = "copyparty";
description = ''
The group that copyparty will run under.
If changed from default, you are responsible for making sure the user exists.
'';
};
openFilesLimit = mkOption {
default = 4096;
type = types.either types.int types.str;
@ -79,22 +114,28 @@ in {
description = ''
Global settings to apply.
Directly maps to values in the [global] section of the copyparty config.
Cannot set "c" or "hist", those are set by this module.
See `${getExe cfg.package} --help` for more details.
'';
default = {
i = "127.0.0.1";
no-reload = true;
hist = externalCacheDir;
};
example = literalExpression ''
{
i = "0.0.0.0";
no-reload = true;
hist = ${externalCacheDir};
}
'';
};
accounts = mkOption {
type = types.attrsOf (types.submodule ({ ... }: {
type = types.attrsOf (
types.submodule (
{ ... }:
{
options = {
passwordFile = mkOption {
type = types.str;
@ -105,7 +146,9 @@ in {
example = "/run/keys/copyparty/ed";
};
};
}));
}
)
);
description = ''
A set of copyparty accounts to create.
'';
@ -118,10 +161,13 @@ in {
};
volumes = mkOption {
type = types.attrsOf (types.submodule ({ ... }: {
type = types.attrsOf (
types.submodule (
{ ... }:
{
options = {
path = mkOption {
type = types.str;
type = types.path;
description = ''
Path of a directory to share.
'';
@ -180,12 +226,16 @@ in {
default = { };
};
};
}));
}
)
);
description = "A set of copyparty volumes to create";
default = {
"/" = {
path = defaultShareDir;
access = { r = "*"; };
access = {
r = "*";
};
};
};
example = literalExpression ''
@ -204,52 +254,63 @@ in {
};
};
config = mkIf cfg.enable {
config = mkIf cfg.enable (
let
command = "${getExe cfg.package} -c ${runtimeConfigPath}";
in
{
systemd.services.copyparty = {
description = "http file sharing hub";
wantedBy = [ "multi-user.target" ];
environment = {
PYTHONUNBUFFERED = "true";
XDG_CONFIG_HOME = "${home}/.config";
XDG_CONFIG_HOME = externalStateDir;
};
preStart = let
replaceSecretCommand = name: attrs:
"${getExe pkgs.replace-secret} '${
passwordPlaceholder name
}' '${attrs.passwordFile}' ${runtimeConfigPath}";
in ''
preStart =
let
replaceSecretCommand =
name: attrs:
"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}";
in
''
set -euo pipefail
install -m 600 ${configFile} ${runtimeConfigPath}
${concatStringsSep "\n"
(mapAttrsToList replaceSecretCommand cfg.accounts)}
${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)}
'';
serviceConfig = {
Type = "simple";
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
ExecStart = command;
# Hardening options
User = "copyparty";
Group = "copyparty";
RuntimeDirectory = name;
User = cfg.user;
Group = cfg.group;
RuntimeDirectory = [ "copyparty" ];
RuntimeDirectoryMode = "0700";
StateDirectory = [ name "${name}/data" "${name}/.config" ];
StateDirectory = [ "copyparty" ];
StateDirectoryMode = "0700";
WorkingDirectory = home;
TemporaryFileSystem = "/:ro";
CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ];
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
WorkingDirectory = externalStateDir;
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/group"
"-/etc/hosts"
"-/etc/localtime"
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
# Would re-mount paths ignored by temporary root
#ProtectSystem = "strict";
ProtectHome = true;
BindPaths =
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
++ [ externalStateDir ]
++ (mapAttrsToList (k: v: v.path) cfg.volumes);
# ProtectSystem = "strict";
# Note that unlike what 'ro' implies,
# this actually makes it impossible to read anything in the root FS,
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
TemporaryFileSystem = "/:ro";
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
@ -269,15 +330,46 @@ in {
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
};
};
users.groups.copyparty = { };
users.users.copyparty = {
# ensure volumes exist:
systemd.tmpfiles.settings."copyparty" = (
lib.attrsets.mapAttrs' (
name: value:
lib.attrsets.nameValuePair (value.path) {
d = {
#: in front of things means it wont change it if the directory already exists.
group = ":${cfg.group}";
user = ":${cfg.user}";
mode = ":755";
};
}
) cfg.volumes
);
users.groups.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";
group = "copyparty";
home = home;
home = externalStateDir;
isSystemUser = true;
};
};
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
(pkgs.writeShellScriptBin "copyparty-hash" ''
set -a # automatically export variables
# set same environment variables as the systemd service
${lib.pipe config.systemd.services.copyparty.environment [
(lib.filterAttrs (n: v: v != null && n != "PATH"))
(lib.mapAttrs (_: v: "${v}"))
(lib.toShellVars)
]}
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
exec ${command} --ah-cli
'')
];
}
);
}

View file

@ -1,57 +1,48 @@
# 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
pkgver="1.16.11"
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=("python" "lsof" "python-jinja")
depends=("bash" "python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"cfssl: generate TLS certificates on startup (pointless when reverse-proxied)"
"cfssl: generate TLS certificates on startup"
"python-mutagen: music tags (alternative)"
"python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
"libkeyfinder-git: detection of musical keys"
"qm-vamp-plugins: BPM detection"
"libkeyfinder: detection of musical keys"
"python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks"
"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")
backup=("etc/${pkgname}.d/init" )
sha256sums=("c8b594e9ca22d81a316ee8b10a9f5f4ebb11f0e4767738e0cb1b1704b13b2d70")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
build() {
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
make
cd "${srcdir}/${pkgname}-${pkgver}"
pushd copyparty/web
make -j$(nproc)
rm Makefile
popd
python3 -m build -wn
python -m build --wheel --no-isolation
}
package() {
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 -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
install -Dm644 "contrib/package/arch/${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/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf"
install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service"
install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service"
install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "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, utillinux, 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)
withHashedPasswords ? true,
# use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true,
# generate TLS certificates on startup (pointless when reverse-proxied)
withCertgen ? false,
# generate TLS certificates on startup (pointless when reverse-proxied)
withCertgen ? false,
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withThumbnails ? true,
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withThumbnails ? true,
# create thumbnails with PyVIPS; even faster, uses more memory
# -- can be combined with Pillow to support more filetypes
withFastThumbnails ? false,
# create thumbnails with PyVIPS; even faster, uses more memory
# -- can be combined with Pillow to support more filetypes
withFastThumbnails ? false,
# 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
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
withMediaProcessing ? true,
# 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
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
withMediaProcessing ? true,
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
withBasicAudioMetadata ? false,
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
withBasicAudioMetadata ? false,
# send ZeroMQ messages from event-hooks
withZeroMQ ? true,
# send ZeroMQ messages from event-hooks
withZeroMQ ? true,
# enable FTPS support in the FTP server
withFTPS ? false,
# enable FTP server
withFTP ? true,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# enable FTPS support in the FTP server
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
pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (ps:
with ps; [
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
buildPythonApplication {
pname = "copyparty";
inherit (pinData) version;
src = fetchurl {
inherit (pinData) url hash;
};
dependencies =
[
jinja2
fusepy
]
++ lib.optional withSMB impacket
++ lib.optional withFTP pyftpdlib
++ lib.optional withFTPS pyopenssl
++ lib.optional withTFTP partftpy
++ lib.optional withCertgen cfssl
++ lib.optional withThumbnails pillow
++ lib.optional withFastThumbnails pyvips
@ -47,21 +95,24 @@ let
++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq
);
in stdenv.mkDerivation {
pname = "copyparty";
version = pinData.version;
src = fetchurl {
url = pinData.url;
hash = pinData.hash;
};
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 ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \
--add-flags "$out/share/copyparty-sfx.py"
++ lib.optional withMagic magic
++ (extraPythonPackages python.pkgs);
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
pyproject = true;
build-system = [
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 ];
};
}

View file

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

View file

@ -11,14 +11,14 @@ import base64
import json
import hashlib
import sys
import re
import tarfile
from pathlib import Path
OUTPUT_FILE = Path("pin.json")
TARGET_ASSET = "copyparty-sfx.py"
TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz"
HASH_TYPE = "sha256"
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):
@ -29,11 +29,13 @@ def get_formatted_hash(binary):
return f"{HASH_TYPE}-{encoded_hash}"
def version_from_sfx(binary):
result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE)
if result:
return result.groups(1)[0].decode("ascii")
def version_from_tar_gz(path):
with tarfile.open(path) as tarball:
release_name = tarball.getmembers()[0].name
prefix = "copyparty-"
if release_name.startswith(prefix):
return release_name.replace(prefix, "")
raise ValueError("version not found in provided file")
@ -42,7 +44,7 @@ def remote_release_pin():
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][0]
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)
@ -52,10 +54,9 @@ def remote_release_pin():
def local_release_pin(path):
asset = path.read_bytes()
version = version_from_sfx(asset)
version = version_from_tar_gz(path)
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}
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

@ -12,6 +12,23 @@ almost the same as minimal-up2k.html except this one...:
-- looks slightly better
========================
== USAGE INSTRUCTIONS ==
1. create a volume which anyone can read from (if you haven't already)
2. copy this file into that volume, so anyone can download it
3. enable the plugin by telling the webbrowser to load this file;
assuming the URL to the public volume is /res/, and
assuming you're using config-files, then add this to your config:
[global]
js-browser: /res/minimal-up2k.js
alternatively, if you're not using config-files, then
add the following commandline argument instead:
--js-browser=/res/minimal-up2k.js
*/
var u2min = `

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]
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
i: 127.0.0.1
[accounts]
ed: wark # username: password
user: password
[/] # create a volume at "/" (the webroot), which will
/mnt # share the contents of the "/mnt" folder
[/]
/var/lib/copyparty-jail
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin
r: *
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

107
contrib/zfs-tune.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
import os
import sqlite3
import sys
import traceback
"""
when the up2k-database is stored on a zfs volume, this may give
slightly higher performance (actual gains not measured yet)
NOTE: must be applied in combination with the related advice in the openzfs documentation;
https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads
and see specifically the SQLite subsection
it is assumed that all databases are stored in a single location,
for example with `--hist /var/store/hists`
three alternatives for running this script:
1. copy it into /var/store/hists and run "python3 zfs-tune.py s"
(s = modify all databases below folder containing script)
2. cd into /var/store/hists and run "python3 ~/zfs-tune.py w"
(w = modify all databases below current working directory)
3. python3 ~/zfs-tune.py /var/store/hists
if you use docker, run copyparty with `--hist /cfg/hists`, copy this script into /cfg, and run this:
podman run --rm -it --entrypoint /usr/bin/python3 ghcr.io/9001/copyparty-ac /cfg/zfs-tune.py s
"""
PAGESIZE = 65536
# borrowed from copyparty; short efficient stacktrace for errors
def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
et, ev, tb = sys.exc_info()
stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]
fmt = "%s:%d <%s>: %s"
ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]
if et or ev or tb:
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
def set_pagesize(db_path):
try:
# check current page_size
with sqlite3.connect(db_path) as db:
v = db.execute("pragma page_size").fetchone()[0]
if v == PAGESIZE:
print(" `-- OK")
return
# https://www.sqlite.org/pragma.html#pragma_page_size
# `- disable wal; set pagesize; vacuum
# (copyparty will reenable wal if necessary)
with sqlite3.connect(db_path) as db:
db.execute("pragma journal_mode=delete")
db.commit()
with sqlite3.connect(db_path) as db:
db.execute(f"pragma page_size = {PAGESIZE}")
db.execute("vacuum")
print(" `-- new pagesize OK")
except Exception:
err = min_ex().replace("\n", "\n -- ")
print(f"FAILED: {db_path}\n -- {err}")
def main():
top = os.path.dirname(os.path.abspath(__file__))
cwd = os.path.abspath(os.getcwd())
try:
x = sys.argv[1]
except:
print(f"""
this script takes one mandatory argument:
specify 's' to start recursing from folder containing this script file ({top})
specify 'w' to start recursing from the current working directory ({cwd})
specify a path to start recursing from there
""")
sys.exit(1)
if x.lower() == "w":
top = cwd
elif x.lower() != "s":
top = x
for dirpath, dirs, files in os.walk(top):
for fname in files:
if not fname.endswith(".db"):
continue
db_path = os.path.join(dirpath, fname)
print(db_path)
set_pagesize(db_path)
if __name__ == "__main__":
main()

View file

@ -63,10 +63,6 @@ web/browser.js
web/browser2.html
web/cf.html
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/easymde.css
web/deps/easymde.js
@ -80,6 +76,7 @@ web/deps/prismd.css
web/deps/scp.woff2
web/deps/sha512.ac.js
web/deps/sha512.hw.js
web/idp.html
web/iiam.gif
web/md.css
web/md.html

View file

@ -40,6 +40,7 @@ from .cfg import flagcats, onedash
from .svchub import SvcHub
from .util import (
APPLESAN_TXT,
BAD_BOTS,
DEF_EXP,
DEF_MTE,
DEF_MTH,
@ -52,19 +53,20 @@ from .util import (
PYFTPD_VER,
RAM_AVAIL,
RAM_TOTAL,
RE_ANSI,
SQLITE_VER,
UNPLICATIONS,
URL_BUG,
URL_PRJ,
Daemon,
align_tab,
ansi_re,
b64enc,
dedent,
has_resource,
load_resource,
min_ex,
pybin,
read_utf8,
termsize,
wrap,
)
@ -91,6 +93,10 @@ u = unicode
printed: list[str] = []
zsid = uuid.uuid4().urn[4:]
CFG_DEF = [os.environ.get("PRTY_CONFIG", "")]
if not CFG_DEF[0]:
CFG_DEF.pop()
class RiceFormatter(argparse.HelpFormatter):
def __init__(self, *args: Any, **kwargs: Any) -> None:
@ -165,7 +171,7 @@ def lprint(*a: Any, **ka: Any) -> None:
txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt)
if not VT100:
txt = ansi_re.sub("", txt)
txt = RE_ANSI.sub("", txt)
print(txt, end="", **ka)
@ -226,7 +232,23 @@ def init_E(EE: EnvParams) -> None:
if E.mod.endswith("__init__"):
E.mod = os.path.dirname(E.mod)
if sys.platform == "win32":
try:
p = os.environ.get("XDG_CONFIG_HOME")
if not p:
raise Exception()
if p.startswith("~"):
p = os.path.expanduser(p)
p = os.path.abspath(os.path.realpath(p))
p = os.path.join(p, "copyparty")
if not os.path.isdir(p):
os.mkdir(p)
os.listdir(p)
except:
p = ""
if p:
E.cfg = p
elif sys.platform == "win32":
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
E.cfg = os.path.normpath(bdir + "/copyparty")
elif sys.platform == "darwin":
@ -255,8 +277,7 @@ def get_srvname(verbose) -> str:
if verbose:
lprint("using hostname from {}\n".format(fp))
try:
with open(fp, "rb") as f:
ret = f.read().decode("utf-8", "replace").strip()
return read_utf8(None, fp, True).strip()
except:
ret = ""
namelen = 5
@ -265,46 +286,17 @@ def get_srvname(verbose) -> str:
ret = re.sub("[234567=]", "", ret)[:namelen]
with open(fp, "wb") as f:
f.write(ret.encode("utf-8") + b"\n")
return ret
def get_fk_salt() -> str:
fp = os.path.join(E.cfg, "fk-salt.txt")
def get_salt(name: str, nbytes: int) -> str:
fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
try:
with open(fp, "rb") as f:
ret = f.read().strip()
return read_utf8(None, fp, True).strip()
except:
ret = b64enc(os.urandom(18))
ret = b64enc(os.urandom(nbytes))
with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8")
def get_dk_salt() -> str:
fp = os.path.join(E.cfg, "dk-salt.txt")
try:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = b64enc(os.urandom(30))
with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8")
def get_ah_salt() -> str:
fp = os.path.join(E.cfg, "ah-salt.txt")
try:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = b64enc(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8")
@ -444,6 +436,40 @@ def args_from_cfg(cfg_path: str) -> list[str]:
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:
msg = [""] * 5
for th in threading.enumerate():
@ -544,7 +570,7 @@ def get_sects():
dedent(
"""
\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
@ -559,17 +585,20 @@ def get_sects():
when running behind a reverse-proxy, it's recommended to
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
permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m
group. This is the best approach. Alternatively,
\033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on
\033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m;
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
access it; bad unless it's inside a restricted folder
\033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone
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
\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
"""
),
],
@ -585,7 +614,7 @@ def get_sects():
--grp takes groupname:username1,username2,...
and groupnames can be used instead of usernames in -v
by prefixing the groupname with %
by prefixing the groupname with @
list of permissions:
"r" (read): list folder contents, download files
@ -614,8 +643,41 @@ def get_sects():
if no accounts or volumes are configured,
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,
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
"""
),
],
@ -767,6 +829,36 @@ def get_sects():
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",
"how to handle url-form POSTs",
@ -875,6 +967,43 @@ def get_sects():
"""
),
],
[
"chmod",
"file/folder permissions",
dedent(
"""
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
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 \033[32m755\033[0m, \033[32m750\033[0m, \033[32m644\033[0m, etc.
first digit = "User"; permission for the unix-user
second digit = "Group"; permission for the unix-group
third digit = "Other"; permission for all other users/groups
for files:
\033[32m0\033[0m = \033[35m---\033[0m = no access
\033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
\033[32m2\033[0m = \033[35m-w-\033[0m = can write
\033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
\033[32m4\033[0m = \033[35mr--\033[0m = can read
\033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
\033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute
for directories/folders:
\033[32m0\033[0m = \033[35m---\033[0m = no access
\033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
\033[32m2\033[0m = \033[35m-w-\033[0m = n/a
\033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
\033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
\033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
\033[32m6\033[0m = \033[35mrw-\033[0m = n/a
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
"""
),
],
[
"pwhash",
"password hashing",
@ -886,6 +1015,9 @@ def get_sects():
copyparty will also hash and print any passwords that are non-hashed
(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
list of optional comma-separated arguments:
@ -963,33 +1095,36 @@ def build_flags_desc():
def add_general(ap, nc, srvname):
ap2 = ap.add_argument_group('general options')
ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file")
ap2 = ap.add_argument_group("general options")
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("-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("-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("--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("-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="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
ap2.add_argument("--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("--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("--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("--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("--version", action="store_true", help="show versions and exit")
def add_qr(ap, tty):
ap2 = ap.add_argument_group('qr options')
ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup")
ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup")
ap2 = ap.add_argument_group("qr options")
ap2.add_argument("--qr", action="store_true", help="show 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("--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("--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("--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):
@ -1003,7 +1138,7 @@ def add_fs(ap):
def add_share(ap):
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-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")
@ -1012,19 +1147,28 @@ def add_share(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("--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-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("--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("--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("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[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("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
ap2.add_argument("--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-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")
@ -1037,26 +1181,30 @@ def add_upload(ap):
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good for low-latency (same-country) connections, 4-8 for android clients, 16 for cross-atlantic (max=64)")
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine")
ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory")
def add_network(ap):
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 = ap.add_argument_group("network options")
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("--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-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("--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("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--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:
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")
else:
elif not MACOS:
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
@ -1067,10 +1215,10 @@ def add_network(ap):
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("--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("--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")
@ -1079,7 +1227,7 @@ def add_tls(ap, cert_path):
def add_cert(ap, 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("--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")
@ -1097,25 +1245,35 @@ def add_cert(ap, cert_path):
def add_auth(ap):
idp_db = os.path.join(E.cfg, "idp.db")
ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, 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 = ap.add_argument_group("IdP / identity provider / user authentication options")
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-hm-usr", metavar="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-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-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-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("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
ap2.add_argument("--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):
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-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-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")
@ -1147,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-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-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-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")
@ -1164,12 +1323,12 @@ def add_zc_ssdp(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("--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("--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-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")
@ -1177,16 +1336,17 @@ def add_ftp(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("--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-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)")
ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of tricky user-agents which expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank")
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("--tftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--tftpv", action="store_true", help="verbose")
@ -1194,12 +1354,12 @@ def add_tftp(ap):
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-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")
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("--smbw", action="store_true", help="enable write support (please dont)")
ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)")
@ -1213,30 +1373,30 @@ def add_smb(ap):
def add_handlers(ap):
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("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing \033[33mPY\033[0m file")
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file")
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")
def add_hooks(ap):
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("--xau", metavar="CMD", type=u, action="append", help="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("--xbc", metavar="CMD", type=u, action="append", help="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("--xbr", metavar="CMD", type=u, action="append", help="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("--xbd", metavar="CMD", type=u, action="append", help="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("--xm", metavar="CMD", type=u, action="append", help="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 = ap.add_argument_group("event hooks (see --help-hooks)")
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="\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="\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="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before 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="\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="\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="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before 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="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m on message")
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")
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("--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)")
@ -1246,35 +1406,45 @@ def add_stats(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("--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("--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):
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("--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-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-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("-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("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\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=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
ap2.add_argument("--ua-nozip", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from download-as-zip/tar; disable with [\033[32mno\033[0m] or blank")
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)")
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
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("-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("--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)")
@ -1288,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("--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-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-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 ++)")
@ -1295,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("--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("--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("--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")
@ -1302,7 +1475,7 @@ def add_safety(ap):
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-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]")
@ -1310,26 +1483,29 @@ def add_salt(ap, fk_salt, dk_salt, ah_salt):
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission")
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)")
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-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")
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("-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("--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-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-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-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")
@ -1338,7 +1514,7 @@ def add_logging(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-rescan", action="store_true", help="disable ?scan (volume reindexing)")
ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
@ -1351,18 +1527,19 @@ def add_admin(ap):
def add_thumbnail(ap):
th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6
th_ram = int(max(min(th_ram, 6), 1) * 10) / 10
ap2 = ap.add_argument_group('thumbnail options')
th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10
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-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("--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-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-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-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-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)")
@ -1371,21 +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-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-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://github.com/libvips/libvips
# https://stackoverflow.com/a/47612661
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,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-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-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,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-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-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,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("--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("--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("--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):
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-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-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")
@ -1393,9 +1576,19 @@ def add_transcoding(ap):
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
def add_tail(ap):
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-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-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")
def add_rss(ap):
ap2 = ap.add_argument_group('RSS options')
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental)")
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-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-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files")
@ -1403,7 +1596,7 @@ def add_rss(ap):
def add_db_general(ap, hcores):
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("-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")
@ -1411,13 +1604,15 @@ def add_db_general(ap, hcores):
ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash")
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("--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-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("--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 (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 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("--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("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)")
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
@ -1430,7 +1625,7 @@ def add_db_general(ap, hcores):
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("-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")
@ -1440,23 +1635,26 @@ 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-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("-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("-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):
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("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
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-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)")
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
ap2.add_argument("--ua-nodoc", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from viewing documents through ?doc=[...]; disable with [\033[32mno\033[0m] or blank")
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-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)")
@ -1474,19 +1672,25 @@ def add_og(ap):
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("--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("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
ap2.add_argument("--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("--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("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
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("--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("--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("--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("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS)
ap2.add_argument("--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("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
@ -1496,20 +1700,22 @@ 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("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-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("--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("--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("--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("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
ap2.add_argument("--have-unlistc", action="store_true", help=argparse.SUPPRESS)
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("--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")
@ -1547,9 +1753,9 @@ def run_argparse(
cert_path = os.path.join(E.cfg, "cert.pem")
fk_salt = get_fk_salt()
dk_salt = get_dk_salt()
ah_salt = get_ah_salt()
fk_salt = get_salt("fk", 18)
dk_salt = get_salt("dk", 30)
ah_salt = get_salt("ah", 18)
# alpine peaks at 5 threads for some reason,
# all others scale past that (but try to avoid SMT),
@ -1591,6 +1797,7 @@ def run_argparse(
add_hooks(ap)
add_stats(ap)
add_txt(ap)
add_tail(ap)
add_og(ap)
add_ui(ap, retry)
add_admin(ap)
@ -1673,16 +1880,7 @@ def main(argv: Optional[list[str]] = None) -> None:
ensure_webdeps()
for k, v in zip(argv[1:], argv[2:]):
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)
argv = expand_cfg(argv)
deprecated: list[tuple[str, str]] = [
("--salt", "--warksalt"),
@ -1708,7 +1906,7 @@ def main(argv: Optional[list[str]] = None) -> None:
argv[idx] = nk + ov
time.sleep(2)
da = len(argv) == 1
da = len(argv) == 1 and not CFG_DEF
try:
if da:
argv.extend(["--qr"])

View file

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

File diff suppressed because it is too large Load diff

View file

@ -9,8 +9,11 @@ from . import path as path
if True: # pylint: disable=using-constant-test
from typing import Any, Optional
_ = (path,)
__all__ = ["path"]
MKD_755 = {"chmod_d": 0o755}
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
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@ -20,19 +23,39 @@ def chmod(p: str, mode: int) -> None:
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]:
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
todo = []
bname = fsenc(name)
try:
os.makedirs(bname, mode)
return True
except:
if not exist_ok or not os.path.isdir(bname):
raise
while bname:
if os.path.isdir(bname):
break
todo.append(bname)
bname = os.path.dirname(bname)
if not todo:
if not exist_ok:
os.mkdir(bname) # to throw
return False
mode = vf["chmod_d"]
chown = "chown" in vf
for zb in todo[::-1]:
try:
os.mkdir(zb, mode)
if chown:
os.chown(zb, vf["uid"], vf["gid"])
except:
if os.path.isdir(zb):
continue
raise
return True
def mkdir(p: str, mode: int = 0o755) -> None:

View file

@ -1,13 +1,11 @@
import calendar
import errno
import filecmp
import json
import os
import shutil
import time
from .__init__ import ANYWIN
from .util import Netdev, load_resource, runcmd, wrename, wunlink
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
@ -122,7 +120,7 @@ def _gen_ca(log: "RootLogger", args):
wunlink(nlog, bname + ".key", VF)
except:
pass
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
wunlink(nlog, bname + ".csr", VF)
log("cert", "new ca OK", 2)
@ -215,7 +213,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
wunlink(nlog, bname + ".key", VF)
except:
pass
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
wunlink(nlog, bname + ".csr", VF)
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:

View file

@ -5,6 +5,9 @@ from __future__ import print_function, unicode_literals
zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv"
onedash = set(zs.split())
# verify that all volflags are documented here:
# grep volflag= __main__.py | sed -r 's/.*volflag=//;s/\).*//' | sort | uniq | while IFS= read -r x; do grep -E "\"$x(=[^ \"]+)?\": \"" cfg.py || printf '%s\n' "$x"; done
def vf_bmap() -> dict[str, str]:
"""argv-to-volflag: simple bools"""
@ -19,6 +22,7 @@ def vf_bmap() -> dict[str, str]:
"no_forget": "noforget",
"no_pipe": "nopipe",
"no_robots": "norobots",
"no_tail": "notail",
"no_thumb": "dthumb",
"no_vthumb": "dvthumb",
"no_athumb": "dathumb",
@ -40,6 +44,7 @@ def vf_bmap() -> dict[str, str]:
"gsel",
"hardlink",
"magic",
"no_db_ip",
"no_sb_md",
"no_sb_lg",
"nsort",
@ -47,10 +52,14 @@ def vf_bmap() -> dict[str, str]:
"og_no_head",
"og_s_title",
"rand",
"reflink",
"rmagic",
"rss",
"wo_up_readme",
"xdev",
"xlink",
"xvol",
"zipmaxu",
):
ret[k] = k
return ret
@ -59,6 +68,7 @@ def vf_bmap() -> dict[str, str]:
def vf_vmap() -> dict[str, str]:
"""argv-to-volflag: simple values"""
ret = {
"ac_convt": "aconvt",
"no_hash": "nohash",
"no_idx": "noidx",
"re_maxage": "scan",
@ -69,14 +79,20 @@ def vf_vmap() -> dict[str, str]:
"th_x3": "th3x",
}
for k in (
"bup_ck",
"chmod_d",
"chmod_f",
"dbd",
"forget_ip",
"hsortn",
"html_head",
"lg_sbf",
"md_sbf",
"lg_sba",
"md_sba",
"md_hist",
"nrand",
"u2ow",
"og_desc",
"og_site",
"og_th",
@ -86,15 +102,29 @@ def vf_vmap() -> dict[str, str]:
"og_title_i",
"og_tpl",
"og_ua",
"put_ck",
"put_name",
"mv_retry",
"rm_retry",
"sort",
"tail_fd",
"tail_rate",
"tail_tmax",
"tail_who",
"tcolor",
"th_spec_p",
"txt_eol",
"unlist",
"u2abort",
"u2ts",
"uid",
"gid",
"unp_who",
"ups_who",
"zip_who",
"zipmaxn",
"zipmaxs",
"zipmaxt",
):
ret[k] = k
return ret
@ -106,6 +136,7 @@ def vf_cmap() -> dict[str, str]:
for k in (
"exp_lg",
"exp_md",
"ext_th",
"mte",
"mth",
"mtp",
@ -144,15 +175,24 @@ flagcats = {
"dedup": "enable symlink-based file deduplication",
"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",
"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",
"noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)",
"chmod_d=755": "unix-permission for new dirs/folders",
"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",
"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",
"nosub": "forces all uploads into the top folder of the vfs",
"magic": "enables filetype detection for nameless uploads",
"gz": "allows server-side gzip of uploads with ?gz (also c,xz)",
"put_name": "fallback filename for nameless uploads",
"put_ck": "default checksum-hasher for PUT/WebDAV uploads",
"bup_ck": "default checksum-hasher for bup/basic uploads",
"gz": "allows server-side gzip compression of uploads with ?gz",
"xz": "allows server-side lzma compression of uploads with ?xz",
"pk": "forces server-side compression, optional arg: xz,9",
},
"upload rules": {
@ -161,8 +201,10 @@ flagcats = {
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
"wo_up_readme": "write-only users can upload logues without getting renamed",
"rand": "force randomized filenames, 9 chars long by default",
"nrand=N": "randomized filenames are N chars long",
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
"u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
"u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
@ -179,17 +221,24 @@ flagcats = {
"e2dsa": "scans all folders for new files on startup; also sets -e2d",
"e2t": "enable multimedia indexing; makes it possible to search for tags",
"e2ts": "scan existing files for tags on startup; also sets -e2t",
"e2tsa": "delete all metadata from DB (full rescan); also sets -e2ts",
"e2tsr": "delete all metadata from DB (full rescan); also sets -e2ts",
"d2ts": "disables metadata collection for existing files",
"e2v": "verify integrity on startup by hashing files and comparing to db",
"e2vu": "when e2v fails, update the db (assume on-disk files are good)",
"e2vp": "when e2v fails, panic and quit copyparty",
"d2ds": "disables onboot indexing, overrides -e2ds*",
"d2t": "disables metadata collection, overrides -e2t*",
"d2v": "disables file verification, overrides -e2v*",
"d2d": "disables all database stuff, overrides -e2*",
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
"dbpath=/tmp/cdb": "puts indexes at that location",
"landmark=foo": "disable db if file foo doesn't exist",
"scan=60": "scan for new files every 60sec, same as --re-maxage",
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
"noforget": "don't forget files when deleted from disk",
"forget_ip=43200": "forget uploader-IP after 30 days (GDPR)",
"no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"xlink": "cross-volume dupe detection / linking (dangerous)",
@ -200,6 +249,8 @@ flagcats = {
"srch_excl": "exclude search results with URL matching this regex",
},
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
"mte=artist,title": "media-tags to index/display",
"mth=fmt,res,ac": "media-tags to hide by default",
"mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)',
"mtp=ahash,vhash=media-hash.py": "collects two tags at once",
},
@ -212,7 +263,10 @@ flagcats = {
"thsize": "thumbnail res; WxH",
"crop": "center-cropping (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",
},
"handlers\n(better explained in --help-handlers)": {
"on404=PY": "handle 404s by executing PY file",
@ -235,10 +289,16 @@ flagcats = {
"grid": "show grid/thumbnails by default",
"gsel": "select files in grid by ctrl-click",
"sort": "default sort order",
"nsort": "natural-sort of leading digits in filenames",
"hsortn": "number of sort-rules to add to media URLs",
"unlist": "dont list files matching REGEX",
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
"nodirsz": "don't show total folder size",
"robots": "allows indexing by search engines (default)",
"norobots": "kindly asks search engines to leave",
"unlistcr": "don't list read-access in controlpanel",
"unlistcw": "don't list write-access in controlpanel",
"no_sb_md": "disable js sandbox for markdown files",
"no_sb_lg": "disable js sandbox for prologue/epilogue",
"sb_md": "enable js sandbox for markdown files (default)",
@ -249,12 +309,51 @@ flagcats = {
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
"nohtml": "return html and markdown as text/html",
},
"opengraph (discord embeds)": {
"og": "enable OG (disables hotlinking)",
"og_site": "sitename; defaults to --name, disable with '-'",
"og_desc": "description text for all files; disable with '-'",
"og_th=jf": "thumbnail format; j / jf / jf3 / w / w3 / ...",
"og_title_a": "audio title format; default: {{ artist }} - {{ title }}",
"og_title_v": "video title format; default: {{ title }}",
"og_title_i": "image title format; default: {{ title }}",
"og_title=foo": "fallback title if there's nothing in the db",
"og_s_title": "force default title; do not read from tags",
"og_tpl": "custom html; see --og-tpl in --help",
"og_no_head": "you want to add tags manually with og_tpl",
"og_ua": "if defined: only send OG html if useragent matches this regex",
},
"textfiles": {
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"exp": "enable textfile expansion; see --help-exp",
"exp_md": "placeholders to expand in markdown files; see --help",
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
"txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)",
},
"tailing": {
"notail": "disable ?tail (download a growing file continuously)",
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
"tail_rate=0.2": "check for new data every 0.2 sec",
"tail_tmax=30": "kill connection after 30 sec",
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
},
"others": {
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
"fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
"dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
"dks": "per-directory accesskeys allow browsing into subdirs",
"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)",
"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",
"zip_who=2": "restrict access to download-as-zip/tar",
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
"zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
"zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
"zipmaxu": "zip-size-limit does not apply to authenticated users",
"nopipe": "disable race-the-beam (download unfinished uploads)",
"mv_retry": "ms-windows: timeout for renaming busy files",
"rm_retry": "ms-windows: timeout for deleting busy files",
"davauth": "ask webdav clients to login for all folders",
@ -264,3 +363,10 @@ flagcats = {
flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()}
if True: # so it gets removed in release-builds
for fun in [vf_bmap, vf_cmap, vf_vmap]:
for k in fun().values():
if k not in flagdescs:
raise Exception("undocumented volflag: " + k)

View file

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

View file

@ -78,7 +78,7 @@ class Fstab(object):
return vid
def build_fallback(self) -> None:
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
self.trusted = False
def build_tab(self) -> None:
@ -111,9 +111,10 @@ class Fstab(object):
tab1.sort(key=lambda x: (len(x[0]), x[0]))
path1, fs1 = tab1[0]
tab = VFS(self.log_func, fs1, path1, AXS(), {})
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
for path, fs in tab1[1:]:
tab.add(fs, path.lstrip("/"))
zs = path.lstrip("/")
tab.add(fs, zs, zs)
self.tab = tab
self.srctab = srctab
@ -130,9 +131,10 @@ class Fstab(object):
if not self.trusted:
# no mtab access; have to build as we go
if "/" in rem:
self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
zs = os.path.join(vn.vpath, rem.split("/")[0])
self.tab.add("idk", zs, zs)
if rem:
self.tab.add(nval, path)
self.tab.add(nval, path, path)
else:
vn.realpath = nval

View file

@ -19,6 +19,7 @@ from .__init__ import PY2, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import (
FN_EMB,
VF_CAREFUL,
Daemon,
ODict,
@ -30,6 +31,7 @@ from .util import (
relchk,
runhook,
sanitize_fn,
set_fperms,
vjoin,
wunlink,
)
@ -81,7 +83,12 @@ class FtpAuth(DummyAuthorizer):
uname = "*"
if username != "anonymous":
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), "")
if zs:
uname = zs
@ -89,6 +96,10 @@ class FtpAuth(DummyAuthorizer):
if args.ipu and uname == "*":
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)):
g = self.hub.gpwd
@ -170,6 +181,16 @@ class FtpFs(AbstractedFS):
fn = sanitize_fn(fn or "", "")
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if (
w
and fn.lower() in FN_EMB
and self.h.uname not in vfs.axs.uread
and "wo_up_readme" not in vfs.flags
):
fn = "_wo_" + fn
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vfs.realpath:
t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath))
@ -218,7 +239,7 @@ class FtpFs(AbstractedFS):
r = "r" in mode
w = "w" in mode or "a" in mode or "+" in mode
ap = self.rv2a(filename, r, w)[0]
ap, vfs, _ = self.rv2a(filename, r, w)
self.validpath(ap)
if w:
try:
@ -250,7 +271,11 @@ class FtpFs(AbstractedFS):
wunlink(self.log, ap, VF_CAREFUL)
return open(fsenc(ap), mode, self.args.iobuf)
ret = open(fsenc(ap), mode, self.args.iobuf)
if w and "fperms" in vfs.flags:
set_fperms(ret, vfs.flags)
return ret
def chdir(self, path: str) -> None:
nwd = join(self.cwd, path)
@ -264,9 +289,12 @@ class FtpFs(AbstractedFS):
# returning 550 is library-default and suitable
raise FSE("No such file or directory")
if vfs.realpath:
avfs = vfs.chk_ap(ap, st)
if not avfs:
raise FSE("Permission denied", 1)
else:
avfs = vfs
self.cwd = nwd
(
@ -281,8 +309,8 @@ class FtpFs(AbstractedFS):
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:
ap = self.rv2a(path, w=True)[0]
bos.makedirs(ap) # filezilla expects this
ap, vfs, _ = self.rv2a(path, w=True)
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path)
@ -381,8 +409,12 @@ class FtpFs(AbstractedFS):
return st
def utime(self, path: str, timeval: float) -> None:
try:
ap = self.rv2a(path, w=True)[0]
return bos.utime(ap, (timeval, timeval))
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:
ap = self.rv2a(path)[0]
@ -471,7 +503,11 @@ class FtpHandler(FTPHandler):
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
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
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
@ -591,7 +627,7 @@ class Ftpd(object):
if "::" in ips:
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:
ips = [x for x in ips if ":" not in x]

File diff suppressed because it is too large Load diff

View file

@ -224,3 +224,6 @@ class HttpConn(object):
if self.u2idx:
self.hsrv.put_u2idx(str(self.addr), self.u2idx)
self.u2idx = None
if self.rproxy:
self.set_rproxy()

View file

@ -70,6 +70,7 @@ from .util import (
build_netmap,
has_resource,
ipnorm,
load_ipr,
load_ipu,
load_resource,
min_ex,
@ -123,6 +124,7 @@ class HttpSrv(object):
self.nm = NetMap([], [])
self.ssdp: Optional["SSDPr"] = None
self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False)
@ -175,6 +177,7 @@ class HttpSrv(object):
"browser",
"browser2",
"cf",
"idp",
"md",
"mde",
"msg",
@ -191,6 +194,11 @@ class HttpSrv(object):
else:
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.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan")
@ -313,6 +321,8 @@ class HttpSrv(object):
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
saddr = ("", 0) # fwd-decl for `except TypeError as ex:`
while not self.stopping:
if self.args.log_conn:
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90")
@ -320,7 +330,8 @@ class HttpSrv(object):
spins = 0
while self.ncli >= self.nclimax:
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
time.sleep(0.1)
@ -394,6 +405,19 @@ class HttpSrv(object):
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
time.sleep(0.02)
continue
except TypeError as ex:
# on macOS, accept() may return a None saddr if blocked by LittleSnitch;
# unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable
if tcp and not saddr:
t = "accept(%s): failed to accept connection from client due to firewall or network issue"
self.log(self.name, t % (fno,), c=3)
try:
sck.close() # type: ignore
except:
pass
time.sleep(0.02)
continue
raise
if self.args.log_conn:
t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(

View file

@ -94,10 +94,21 @@ class Ico(object):
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g>
<rect width="100%" height="100%" fill="#{}" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
<text x="50%" y="{}" dominant-baseline="middle" text-anchor="middle" xml:space="preserve"
fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text>
</g></svg>
"""
svg = svg.format(h, c[:6], c[6:], html_escape(ext, True))
txt = html_escape(ext, True)
if "\n" in txt:
lines = txt.split("\n")
n = len(lines)
y = "20%" if n == 2 else "10%" if n == 3 else "0"
zs = '<tspan x="50%%" dy="1.2em">%s</tspan>'
txt = "".join([zs % (x,) for x in lines])
else:
y = "50%"
svg = svg.format(h, c[:6], y, c[6:], txt)
return "image/svg+xml", svg.encode("utf-8")

View file

@ -76,7 +76,8 @@ class MDNS(MCast):
if not self.args.zm_nwa_1:
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")
self.hn = "-".join(x for x in zs.split("?") if x) or (
"vault-{}".format(random.randint(1, 255))

View file

@ -18,6 +18,7 @@ from .util import (
REKOBO_LKEY,
VF_CAREFUL,
fsenc,
gzip,
min_ex,
pybin,
retchk,
@ -28,7 +29,7 @@ from .util import (
)
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
@ -66,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_01 = re.compile(r"(^|[^0-9v])0+[01]\b")
FMT_AU = set("mp3 ogg flac wav".split())
class MParser(object):
def __init__(self, cmdline: str) -> None:
@ -138,8 +141,6 @@ def au_unpk(
fd, ret = tempfile.mkstemp("." + au)
if pk == "gz":
import gzip
fi = gzip.GzipFile(abspath, mode="rb")
elif pk == "xz":
@ -167,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 CBZ_01.search(x[0])] or znil
t = "cbz: %d files, %d hits" % (nf, len(znil))
using = sorted(znil)[0][1].filename
if znil:
t += ", using " + znil[0][1].filename
t += ", using " + using
log(t)
if not znil:
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:
raise Exception("unknown compression %s" % (pk,))
@ -203,7 +208,7 @@ def au_unpk(
def ffprobe(
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 = [
b"ffprobe",
b"-hide_banner",
@ -217,8 +222,17 @@ def ffprobe(
return parse_ffprobe(so)
def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
"""ffprobe -show_format -show_streams"""
def parse_ffprobe(
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 = []
fmt = {}
g = {}
@ -242,7 +256,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
ret: dict[str, Any] = {} # processed
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"]:
is_audio = True
@ -270,6 +284,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
["channel_layout", "chs"],
["sample_rate", ".hz"],
["bit_rate", ".aq"],
["bits_per_sample", ".bps"],
["bits_per_raw_sample", ".bprs"],
["duration", ".dur"],
]
@ -309,7 +325,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
ret[rk] = v1
if ret.get("vc") == "ansi": # shellscript
return {}, {}
return {}, {}, [], {}
for strm in streams:
for sk, sv in strm.items():
@ -358,7 +374,77 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[
zero = int("0")
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):
@ -629,7 +715,7 @@ class MTag(object):
if not bos.path.isfile(abspath):
return {}
ret, md = ffprobe(abspath, self.args.mtag_to)
ret, md, _, _ = ffprobe(abspath, self.args.mtag_to)
if self.args.mtag_vv:
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)
# gvfs breaks if a linklocal ip appears in a dns reply
ll = {
k: v
for k, v in srv.ips.items()
if k.startswith("169.254") or k.startswith("fe80")
}
ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
rt = {k: v for k, v in srv.ips.items() if k not in ll}
if self.args.ll or not rt:

View file

@ -15,7 +15,7 @@ try:
raise Exception()
HAVE_ARGON2 = True
from argon2 import __version__ as argon2ver
from argon2 import exceptions as argon2ex
except:
HAVE_ARGON2 = False
@ -147,6 +147,10 @@ class PWHash(object):
def cli(self) -> None:
import getpass
if self.args.usernames:
t = "since you have enabled --usernames, please provide username:password"
print(t)
while True:
try:
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)
try:
bos.makedirs(ap2)
bos.makedirs(ap2, vf=vfs2.flags)
except:
pass
@ -334,7 +334,7 @@ class SMB(object):
t = "blocked mkdir (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.mkdir(ap)
return bos.mkdir(ap, vfs.flags["chmod_d"])
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
try:

View file

@ -17,6 +17,9 @@ if True: # pylint: disable=using-constant-test
from .util import NamedLogger
TAR_NO_OPUS = set("aac|m4a|mp3|oga|ogg|opus|wma".split("|"))
class StreamArc(object):
def __init__(
self,
@ -82,9 +85,7 @@ def enthumb(
) -> dict[str, Any]:
rem = f["vp"]
ext = rem.rsplit(".", 1)[-1].lower()
if (fmt == "mp3" and ext == "mp3") or (
fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|")
):
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
raise Exception()
vp = vjoin(vtop, rem.split("/", 1)[1])

View file

@ -2,8 +2,8 @@
from __future__ import print_function, unicode_literals
import argparse
import atexit
import errno
import gzip
import logging
import os
import re
@ -28,6 +28,7 @@ if True: # pylint: disable=using-constant-test
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv
from .bos import bos
from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2
@ -38,6 +39,7 @@ from .th_srv import (
HAVE_FFPROBE,
HAVE_HEIF,
HAVE_PIL,
HAVE_RAW,
HAVE_VIPS,
HAVE_WEBP,
ThumbSrv,
@ -51,6 +53,7 @@ from .util import (
HAVE_PSUTIL,
HAVE_SQLITE3,
HAVE_ZMQ,
RE_ANSI,
URL_BUG,
UTC,
VERSIONS,
@ -60,19 +63,25 @@ from .util import (
HMaccas,
ODict,
alltrace,
ansi_re,
build_netmap,
expat_ver,
gzip,
load_ipr,
load_ipu,
lock_file,
min_ex,
mp,
odfusion,
pybin,
start_log_thrs,
start_stackmon,
termsize,
ub64enc,
)
if HAVE_SQLITE3:
import sqlite3
if TYPE_CHECKING:
try:
from .mdns import MDNS
@ -84,6 +93,11 @@ if PY2:
range = xrange # type: ignore
VER_IDP_DB = 1
VER_SESSION_DB = 1
VER_SHARES_DB = 2
class SvcHub(object):
"""
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
@ -142,6 +156,7 @@ class SvcHub(object):
args.no_del = True
args.no_mv = True
args.hardlink = True
args.dav_auth = True
args.vague_403 = True
args.nih = True
@ -158,6 +173,7 @@ class SvcHub(object):
# for non-http clients (ftp, tftp)
self.bans: dict[str, int] = {}
self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False)
@ -186,8 +202,14 @@ class SvcHub(object):
if not args.use_fpool and args.j != 1:
args.no_fpool = True
t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems"
self.log("root", t.format(args.j))
t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems, and make some antivirus-softwares "
c = 0
if ANYWIN:
t += "(especially Microsoft Defender) stress your CPU and HDD severely during big uploads"
c = 3
else:
t += "consume more resources (CPU/HDD) than normal"
self.log("root", t.format(args.j), c)
if not args.no_fpool and args.j != 1:
t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled"
@ -223,7 +245,7 @@ class SvcHub(object):
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)
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"
self.log("root", t, 1)
raise Exception(t)
@ -239,8 +261,24 @@ class SvcHub(object):
setattr(args, "ipu_iu", iu)
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():
if getattr(args, "show_%s" % (zs,)):
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
if args.ah_cli or args.ah_gen:
args.idp_store = 0
args.no_ses = True
args.shr = ""
if args.idp_store and args.have_idp_hdrs:
self.setup_db("idp")
if not self.args.no_ses:
self.setup_session_db()
self.setup_db("ses")
args.shr1 = ""
if args.shr:
@ -292,6 +330,8 @@ class SvcHub(object):
decs.pop("vips", None)
if not HAVE_PIL:
decs.pop("pil", None)
if not HAVE_RAW:
decs.pop("raw", None)
if not HAVE_FFMPEG or not HAVE_FFPROBE:
decs.pop("ff", None)
@ -398,34 +438,95 @@ class SvcHub(object):
getattr(args, zs).mutex = threading.Lock()
except:
pass
if args.ipr:
for nm in args.ipr_u.values():
nm.mutex = threading.Lock()
def setup_session_db(self) -> None:
if not HAVE_SQLITE3:
def _db_onfail_ses(self) -> None:
self.args.no_ses = True
t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
self.log("root", t, 3)
def _db_onfail_idp(self) -> None:
self.args.idp_store = 0
def setup_db(self, which: str) -> None:
"""
the "non-mission-critical" databases; if something looks broken then just nuke it
"""
if which == "ses":
native_ver = VER_SESSION_DB
db_path = self.args.ses_db
desc = "sessions-db"
pathopt = "ses-db"
sanchk_q = "select count(*) from us"
createfun = self._create_session_db
failfun = self._db_onfail_ses
elif which == "idp":
native_ver = VER_IDP_DB
db_path = self.args.idp_db
desc = "idp-db"
pathopt = "idp-db"
sanchk_q = "select count(*) from us"
createfun = self._create_idp_db
failfun = self._db_onfail_idp
else:
raise Exception("unknown cachetype")
if not db_path.endswith(".db"):
zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db"
self.log("root", zs % (pathopt, desc, db_path), 1)
raise Exception(BAD_CFG)
if not HAVE_SQLITE3:
failfun()
if which == "ses":
zs = "disabling sessions, will use plaintext passwords in cookies"
elif which == "idp":
zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart"
self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3)
return
import sqlite3
assert sqlite3 # type: ignore # !rm
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
create = True
db_path = self.args.ses_db
self.log("root", "opening sessions-db %s" % (db_path,))
for n in range(2):
zs = "creating new" if create else "opening"
self.log("root", "%s %s %s" % (zs, desc, db_path))
for tries in range(2):
sver = 0
try:
db = sqlite3.connect(db_path)
cur = db.cursor()
try:
cur.execute("select count(*) from us").fetchone()
create = False
break
zs = "select v from kv where k='sver'"
sver = cur.execute(zs).fetchall()[0][0]
if sver > native_ver:
zs = "this version of copyparty only understands %s v%d and older; the db is v%d"
raise Exception(zs % (desc, native_ver, sver))
cur.execute(sanchk_q).fetchone()
except:
pass
except Exception as ex:
if n:
if sver:
raise
t = "sessions-db corrupt; deleting and recreating: %r"
self.log("root", t % (ex,), 3)
sver = createfun(cur)
err = self._verify_db(
cur, which, pathopt, db_path, desc, sver, native_ver
)
if err:
tries = 99
self.args.no_ses = True
self.log("root", err, 3)
break
except Exception as ex:
if tries or sver > native_ver:
raise
t = "%s is unusable; deleting and recreating: %r"
self.log("root", t % (desc, ex), 3)
try:
cur.close() # type: ignore
except:
@ -434,8 +535,13 @@ class SvcHub(object):
db.close() # type: ignore
except:
pass
try:
os.unlink(db_lock)
except:
pass
os.unlink(db_path)
def _create_session_db(self, cur: "sqlite3.Cursor") -> int:
sch = [
r"create table kv (k text, v int)",
r"create table us (un text, si text, t0 int)",
@ -445,17 +551,74 @@ class SvcHub(object):
r"create index us_t0 on us(t0)",
r"insert into kv values ('sver', 1)",
]
assert db # type: ignore # !rm
assert cur # type: ignore # !rm
if create:
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new sessions-db")
db.commit()
return 1
def _create_idp_db(self, cur: "sqlite3.Cursor") -> int:
sch = [
r"create table kv (k text, v int)",
r"create table us (un text, gs text)",
# username, groups
r"create index us_un on us(un)",
r"insert into kv values ('sver', 1)",
]
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new idp-db")
return 1
def _verify_db(
self,
cur: "sqlite3.Cursor",
which: str,
pathopt: str,
db_path: str,
desc: str,
sver: int,
native_ver: int,
) -> str:
# ensure writable (maybe owned by other user)
db = cur.connection
try:
zil = cur.execute("select v from kv where k='pid'").fetchall()
if len(zil) > 1:
raise Exception()
owner = zil[0][0]
except:
owner = 0
if which == "ses":
cons = "Will now disable sessions and instead use plaintext passwords in cookies."
elif which == "idp":
cons = "Each IdP-volume will not become available until its associated user sends their first request."
else:
raise Exception()
if not lock_file(db_path + ".lock"):
t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s"
return t % (desc, db_path, owner, pathopt, cons)
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
if owner:
# wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720
for k, v in vars:
cur.execute("update kv set v=? where k=?", (v, k))
else:
# wear-estimate: 3~4 cells; offsets 0x10, 0x50, 0x19180, 0x19710, 0x36000, 0x360b0, 0x36b90
for k, v in vars:
cur.execute("insert into kv values(?, ?)", (k, v))
if sver < native_ver:
cur.execute("delete from kv where k='sver'")
cur.execute("insert into kv values('sver',?)", (native_ver,))
db.commit()
cur.close()
db.close()
return ""
def setup_share_db(self) -> None:
al = self.args
@ -464,7 +627,7 @@ class SvcHub(object):
al.shr = ""
return
import sqlite3
assert sqlite3 # type: ignore # !rm
al.shr = al.shr.strip("/")
if "/" in al.shr or not al.shr:
@ -475,34 +638,48 @@ class SvcHub(object):
al.shr = "/%s/" % (al.shr,)
al.shr1 = al.shr[1:]
create = True
modified = False
# policy:
# the shares-db is important, so panic if something is wrong
db_path = self.args.shr_db
self.log("root", "opening shares-db %s" % (db_path,))
for n in range(2):
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
create = True
zs = "creating new" if create else "opening"
self.log("root", "%s shares-db %s" % (zs, db_path))
sver = 0
try:
db = sqlite3.connect(db_path)
cur = db.cursor()
try:
if not create:
zs = "select v from kv where k='sver'"
sver = cur.execute(zs).fetchall()[0][0]
if sver > VER_SHARES_DB:
zs = "this version of copyparty only understands shares-db v%d and older; the db is v%d"
raise Exception(zs % (VER_SHARES_DB, sver))
cur.execute("select count(*) from sh").fetchone()
create = False
break
except:
pass
except Exception as ex:
if n:
t = "could not open shares-db; will now panic...\nthe following database must be repaired or deleted before you can launch copyparty:\n%s\n\nERROR: %s\n\nadditional details:\n%s\n"
self.log("root", t % (db_path, ex, min_ex()), 1)
raise
t = "shares-db corrupt; deleting and recreating: %r"
self.log("root", t % (ex,), 3)
try:
cur.close() # type: ignore
zil = cur.execute("select v from kv where k='pid'").fetchall()
if len(zil) > 1:
raise Exception()
owner = zil[0][0]
except:
pass
try:
db.close() # type: ignore
except:
pass
os.unlink(db_path)
owner = 0
if not lock_file(db_lock):
t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic."
t = t % (db_path, owner)
self.log("root", t, 1)
raise Exception(t)
sch1 = [
r"create table kv (k text, v int)",
@ -514,34 +691,37 @@ class SvcHub(object):
r"create index sf_k on sf(k)",
r"create index sh_k on sh(k)",
r"create index sh_t1 on sh(t1)",
r"insert into kv values ('sver', 2)",
]
assert db # type: ignore # !rm
assert cur # type: ignore # !rm
if create:
dver = 2
modified = True
if not sver:
sver = VER_SHARES_DB
for cmd in sch1 + sch2:
cur.execute(cmd)
self.log("root", "created new shares-db")
else:
(dver,) = cur.execute("select v from kv where k = 'sver'").fetchall()[0]
if dver == 1:
modified = True
if sver == 1:
for cmd in sch2:
cur.execute(cmd)
cur.execute("update sh set st = 0")
self.log("root", "shares-db schema upgrade ok")
if modified:
for cmd in [
r"delete from kv where k = 'sver'",
r"insert into kv values ('sver', %d)" % (2,),
]:
cur.execute(cmd)
db.commit()
if sver < VER_SHARES_DB:
cur.execute("delete from kv where k='sver'")
cur.execute("insert into kv values('sver',?)", (VER_SHARES_DB,))
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
if owner:
# wear-estimate: same as sessions-db
for k, v in vars:
cur.execute("update kv set v=? where k=?", (v, k))
else:
for k, v in vars:
cur.execute("insert into kv values(?, ?)", (k, v))
db.commit()
cur.close()
db.close()
@ -606,6 +786,39 @@ class SvcHub(object):
def sigterm(self) -> 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:
self.httpsrv_up += 1
if self.httpsrv_up != self.broker.num_workers:
@ -618,6 +831,9 @@ class SvcHub(object):
break
if self.tcpsrv.qr:
if self.args.qr_pin:
self.sticky_qr()
else:
self.log("qr-code", self.tcpsrv.qr)
else:
self.log("root", "workers OK\n")
@ -645,6 +861,7 @@ class SvcHub(object):
(HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"),
(HAVE_HEIF, "pillow-heif", "read .heif images with pillow (rarely useful)"),
(HAVE_AVIF, "pillow-avif", "read .avif images with pillow (rarely useful)"),
(HAVE_RAW, "rawpy", "read RAW images"),
]
if ANYWIN:
to_check += [
@ -679,19 +896,11 @@ class SvcHub(object):
t += ", "
t += "\033[0mNG: \033[35m" + sng
t += "\033[0m, see --deps"
self.log("dependencies", t, 6)
t += "\033[0m, see --deps (this is fine btw)"
self.log("optional-dependencies", t, 6)
def _check_env(self) -> None:
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)
al = self.args
if self.args.no_bauth:
t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead"
@ -699,6 +908,21 @@ class SvcHub(object):
if self.args.bauth_last:
self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3)
have_tcp = False
for zs in al.i:
if not zs.startswith(("unix:", "fd:")):
have_tcp = True
if not have_tcp:
zb = False
zs = "z zm zm4 zm6 zmv zmvv zs zsv zv"
for zs in zs.split():
if getattr(al, zs, False):
setattr(al, zs, False)
zb = True
if zb:
t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested"
self.log("root", t, 3)
if not self.args.no_dav:
from .dxml import DXML_OK
@ -763,13 +987,20 @@ class SvcHub(object):
vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl]
setattr(al, k, vl)
for k in "lo hist ssl_log".split(" "):
for k in "lo hist dbpath ssl_log".split(" "):
vs = getattr(al, k)
if vs:
vs = os.path.expandvars(os.path.expanduser(vs))
setattr(al, k, vs)
for k in "sus_urls nonsus_urls".split(" "):
for k in "idp_adm".split(" "):
vs = getattr(al, k)
vsa = [x.strip() for x in vs.split(",")]
vsa = [x.lower() for x in vsa if x]
setattr(al, k + "_set", set(vsa))
zs = "dav_ua1 sus_urls nonsus_urls ua_nodoc ua_nozip"
for k in zs.split(" "):
vs = getattr(al, k)
if not vs or vs == "no":
setattr(al, k, None)
@ -789,10 +1020,23 @@ class SvcHub(object):
al.sus_urls = None
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_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.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa, True)
@ -837,6 +1081,8 @@ class SvcHub(object):
except:
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
al.js_utc = "false" if al.localtime else "true"
al.tcolor = al.tcolor.lstrip("#")
if len(al.tcolor) == 3: # fc5 => ffcc55
al.tcolor = "".join([x * 2 for x in al.tcolor])
@ -931,7 +1177,7 @@ class SvcHub(object):
fn = sel_fn
try:
os.makedirs(os.path.dirname(fn))
bos.makedirs(os.path.dirname(fn))
except:
pass
@ -948,6 +1194,9 @@ class SvcHub(object):
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
if getattr(self.args, "free_umask", False):
os.fchmod(lh.fileno(), 0o644)
argv = [pybin] + self.argv
if hasattr(shlex, "quote"):
argv = [shlex.quote(x) for x in argv]
@ -1215,11 +1464,18 @@ class SvcHub(object):
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
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:
msg = ansi_re.sub("", msg)
msg = RE_ANSI.sub("", msg)
if "\033" in src:
src = ansi_re.sub("", src)
src = RE_ANSI.sub("", src)
elif c:
if isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg)
@ -1260,7 +1516,7 @@ class SvcHub(object):
raise
def check_mp_support(self) -> str:
if MACOS:
if MACOS and not os.environ.get("PRTY_FORCE_MP"):
return "multiprocessing is wonky on mac osx;"
elif sys.version_info < (3, 3):
return "need python 3.3 or newer for multiprocessing;"
@ -1280,7 +1536,7 @@ class SvcHub(object):
return False
try:
if mp.cpu_count() <= 1:
if mp.cpu_count() <= 1 and not os.environ.get("PRTY_FORCE_MP"):
raise Exception()
except:
self.log("svchub", "only one CPU detected; multiprocessing disabled")

View file

@ -4,12 +4,11 @@ from __future__ import print_function, unicode_literals
import calendar
import stat
import time
import zlib
from .authsrv import AuthSrv
from .bos import bos
from .sutil import StreamArc, errdesc
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile, zlib
if True: # pylint: disable=using-constant-test
from typing import Any, Generator, Optional
@ -55,6 +54,7 @@ def gen_fdesc(sz: int, crc32: int, z64: bool) -> bytes:
def gen_hdr(
h_pos: Optional[int],
z64: bool,
fn: str,
sz: int,
lastmod: int,
@ -71,7 +71,6 @@ def gen_hdr(
# appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64
# extinfo for values which exceed H, but that becomes an off-by-one
# (can't tell if it was clamped or exactly maxval), make it obvious
z64 = sz >= 0xFFFFFFFF
z64v = [sz, sz] if z64 else []
if h_pos and h_pos >= 0xFFFFFFFF:
# central, also consider ptr to original header
@ -245,6 +244,7 @@ class StreamZip(StreamArc):
sz = st.st_size
ts = st.st_mtime
h_pos = self.pos
crc = 0
if self.pre_crc:
@ -253,8 +253,12 @@ class StreamZip(StreamArc):
crc &= 0xFFFFFFFF
h_pos = self.pos
buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc)
# some unzip-programs expect a 64bit data-descriptor
# even if the only 32bit-exceeding value is the offset,
# so force that by placeholdering the filesize too
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
buf = gen_hdr(None, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
yield self._ct(buf)
for buf in yieldfile(src, self.args.iobuf):
@ -267,8 +271,6 @@ class StreamZip(StreamArc):
self.items.append((name, sz, ts, crc, h_pos))
z64 = sz >= 4 * 1024 * 1024 * 1024
if z64 or not self.pre_crc:
buf = gen_fdesc(sz, crc, z64)
yield self._ct(buf)
@ -307,7 +309,8 @@ class StreamZip(StreamArc):
cdir_pos = self.pos
for name, sz, ts, crc, h_pos in self.items:
buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc)
z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF
buf = gen_hdr(h_pos, z64, name, sz, ts, self.utf8, crc, self.pre_crc)
mbuf += self._ct(buf)
if len(mbuf) >= 16384:
yield mbuf

View file

@ -25,8 +25,8 @@ from .util import (
termsize,
)
if True:
from typing import Generator, Union
if True: # pylint: disable=using-constant-test
from typing import Generator, Optional, Union
if TYPE_CHECKING:
from .svchub import SvcHub
@ -151,9 +151,15 @@ class TcpSrv(object):
if just_ll or self.args.ll:
ll_ok.add(ip.split("/")[0])
listening_on = []
for ip, ports in sorted(ok.items()):
for port in sorted(ports):
listening_on.append("%s %s" % (ip, port))
qr1: dict[str, list[int]] = {}
qr2: dict[str, list[int]] = {}
msgs = []
accessible_on = []
title_tab: dict[str, dict[str, int]] = {}
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
@ -169,6 +175,10 @@ class TcpSrv(object):
):
continue
zs = "%s %s" % (ip, port)
if zs not in accessible_on:
accessible_on.append(zs)
proto = " http"
if self.args.http_only:
pass
@ -219,6 +229,14 @@ class TcpSrv(object):
else:
print("\n", end="")
for fn, ls in (
(self.args.wr_h_eps, listening_on),
(self.args.wr_h_aon, accessible_on),
):
if fn:
with open(fn, "wb") as f:
f.write(("\n".join(ls)).encode("utf-8"))
if self.args.qr or self.args.qrs:
self.qr = self._qr(qr1, qr2)
@ -227,8 +245,10 @@ class TcpSrv(object):
def _listen(self, ip: str, port: int) -> None:
uds_perm = uds_gid = -1
if "unix:" in ip:
bound: Optional[socket.socket] = None
tcp = False
if "unix:" in ip:
ipv = socket.AF_UNIX
uds = ip.split(":")
ip = uds[-1]
@ -241,7 +261,12 @@ class TcpSrv(object):
import grp
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:
tcp = True
ipv = socket.AF_INET6
@ -249,7 +274,7 @@ class TcpSrv(object):
tcp = True
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:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -264,9 +289,13 @@ class TcpSrv(object):
except:
pass # will create another ipv4 socket instead
if not ANYWIN and self.args.freebind:
if getattr(self.args, "freebind", False):
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
if bound:
self.srv.append(srv)
return
try:
if tcp:
srv.bind((ip, port))
@ -419,7 +448,7 @@ class TcpSrv(object):
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
from .stolen.ifaddr import get_adapters
listen_ips = [x for x in listen_ips if "unix:" not in x]
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
nics = get_adapters(True)
eps: dict[str, Netdev] = {}
@ -548,7 +577,7 @@ class TcpSrv(object):
ip = None
ips = list(t1) + list(t2)
qri = self.args.qri
if self.args.zm and not qri:
if self.args.zm and not qri and ips:
name = self.args.name + ".local"
t1[name] = next(v for v in (t1 or t2).values())
ips = [name] + ips
@ -565,8 +594,7 @@ class TcpSrv(object):
if not ip:
return ""
if ":" in ip:
ip = "[{}]".format(ip)
hip = "[%s]" % (ip,) if ":" in ip else ip
if self.args.http_only:
https = ""
@ -578,7 +606,7 @@ class TcpSrv(object):
ports = t1.get(ip, t2.get(ip, []))
dport = 443 if https else 80
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")
if PY2:
@ -586,6 +614,10 @@ class TcpSrv(object):
fg = self.args.qr_fg
bg = self.args.qr_bg
nocolor = fg == -1
if nocolor:
fg = 0
pad = self.args.qrp
zoom = self.args.qrz
qrc = QrCode.encode_binary(btxt)
@ -613,6 +645,8 @@ class TcpSrv(object):
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"
if nocolor:
cc = " \033[0m"
t = cc + "\n{2}\033[999G\033[0m\033[J"
t = t.format(fg, bg, qr)
if ANYWIN:

View file

@ -36,7 +36,20 @@ from partftpy.TftpShared import TftpException
from .__init__ import EXE, PY2, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import UTC, BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot
from .util import (
FN_EMB,
UTC,
BytesIO,
Daemon,
ODict,
exclude_dotfiles,
min_ex,
runhook,
set_fperms,
undot,
vjoin,
vsplit,
)
if True: # pylint: disable=using-constant-test
from typing import Any, Union
@ -166,7 +179,7 @@ class Tftpd(object):
if "::" in ips:
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:
ips = [x for x in ips if ":" not in x]
@ -244,16 +257,25 @@ class Tftpd(object):
for srv in srvs:
srv.stop()
def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
def _v2a(
self, caller: str, vpath: str, perms: list, *a: Any
) -> tuple[VFS, str, str]:
vpath = vpath.replace("\\", "/").lstrip("/")
if not perms:
perms = [True, True]
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags:
zs, fn = vsplit(vpath)
if fn.lower() in FN_EMB:
vpath = vjoin(zs, "_wo_" + fn)
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
if not vfs.realpath:
raise Exception("unmapped vfs")
return vfs, vfs.canonical(rem)
return vfs, vpath, vfs.canonical(rem)
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
# generate file listing if vpath is dir.txt and return as file object
@ -263,6 +285,7 @@ class Tftpd(object):
if not ptn or not ptn.match(fn.lower()):
return None
tsdt = datetime.fromtimestamp
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
fsroot, vfs_ls, vfs_virt = vn.ls(
rem,
@ -275,7 +298,7 @@ class Tftpd(object):
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
real1 = dirs1 + fils1
realt = [(datetime.fromtimestamp(mt, UTC), sz, fn) for mt, sz, fn in real1]
realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1]
reals = [
(
"%04d-%02d-%02d %02d:%02d:%02d"
@ -331,7 +354,7 @@ class Tftpd(object):
else:
raise Exception("bad mode %s" % (mode,))
vfs, ap = self._v2a("open", vpath, [rd, wr])
vfs, vpath, ap = self._v2a("open", vpath, [rd, wr])
if wr:
if "*" not in vfs.axs.uwrite:
yeet("blocked write; folder not world-writable: /%s" % (vpath,))
@ -365,18 +388,24 @@ class Tftpd(object):
if not a:
a = (self.args.iobuf,)
return open(ap, mode, *a, **ka)
ret = open(ap, mode, *a, **ka)
if wr and "fperms" in vfs.flags:
set_fperms(ret, vfs.flags)
return ret
def _mkdir(self, vpath: str, *a) -> None:
vfs, ap = self._v2a("mkdir", vpath, [])
vfs, _, ap = self._v2a("mkdir", vpath, [False, True])
if "*" not in vfs.axs.uwrite:
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
return bos.mkdir(ap)
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:
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
vfs, ap = self._v2a("delete", vpath, [True, False, False, True])
vfs, _, ap = self._v2a("delete", vpath, [True, False, False, True])
try:
inf = bos.stat(ap)
@ -400,7 +429,7 @@ class Tftpd(object):
def _p_exists(self, vpath: str) -> bool:
try:
ap = self._v2a("p.exists", vpath, [False, False])[1]
ap = self._v2a("p.exists", vpath, [False, False])[2]
bos.stat(ap)
return True
except:
@ -408,7 +437,7 @@ class Tftpd(object):
def _p_isdir(self, vpath: str) -> bool:
try:
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1])
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[2])
ret = stat.S_ISDIR(st.st_mode)
return ret
except:

View file

@ -1,13 +1,15 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import errno
import os
import stat
from .__init__ import TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .th_srv import EXTS_AC, HAVE_WEBP, thumb_path
from .util import Cooldown
from .util import Cooldown, Pebkac
if True: # pylint: disable=using-constant-test
from typing import Optional, Union
@ -16,6 +18,9 @@ if TYPE_CHECKING:
from .httpsrv import HttpSrv
IOERROR = "reading the file was denied by the server os; either due to filesystem permissions, selinux, apparmor, or similar:\n%r"
class ThumbCli(object):
def __init__(self, hsrv: "HttpSrv") -> None:
self.broker = hsrv.broker
@ -31,11 +36,15 @@ class ThumbCli(object):
if not c:
raise Exception()
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.fmt_pil = c["pil"]
self.fmt_vips = c["vips"]
self.fmt_raw = c["raw"]
self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"]
@ -83,7 +92,7 @@ class ThumbCli(object):
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]:
return os.path.join(ptop, rem)
if fmt[:1] in "jw":
if fmt[:1] in "jw" and fmt != "wav":
sfmt = fmt[:1]
if sfmt == "j" and self.args.th_no_jpg:
@ -124,7 +133,7 @@ class ThumbCli(object):
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
tpaths = [tpath]
if fmt == "w":
if fmt[:1] == "w" and fmt != "wav":
# also check for jpg (maybe webp is unavailable)
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
@ -157,8 +166,22 @@ class ThumbCli(object):
if abort:
return None
if not bos.path.getsize(os.path.join(ptop, rem)):
ap = os.path.join(ptop, rem)
try:
st = bos.stat(ap)
if not st.st_size or not stat.S_ISREG(st.st_mode):
return None
with open(ap, "rb", 4) as f:
if not f.read(4):
raise Exception()
except OSError as ex:
if ex.errno == errno.ENOENT:
raise Pebkac(404)
else:
raise Pebkac(500, IOERROR % (ex,))
except Exception as ex:
raise Pebkac(500, IOERROR % (ex,))
x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt)
return x.get() # type: ignore

View file

@ -2,10 +2,13 @@
from __future__ import print_function, unicode_literals
import hashlib
import io
import logging
import os
import re
import shutil
import subprocess as sp
import tempfile
import threading
import time
@ -18,16 +21,17 @@ from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
from .util import BytesIO # type: ignore
from .util import (
FFMPEG_URL,
VF_CAREFUL,
Cooldown,
Daemon,
afsenc,
atomic_move,
fsenc,
min_ex,
runcmd,
statdir,
ub64enc,
vsplit,
wrename,
wunlink,
)
@ -47,7 +51,11 @@ HAVE_AVIF = False
HAVE_WEBP = False
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())
PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
try:
if os.environ.get("PRTY_NO_PIL"):
@ -78,6 +86,9 @@ try:
if os.environ.get("PRTY_NO_PIL_HEIF"):
raise Exception()
try:
from pillow_heif import register_heif_opener
except ImportError:
from pyheif_pillow_opener import register_heif_opener
register_heif_opener()
@ -89,6 +100,10 @@ try:
if os.environ.get("PRTY_NO_PIL_AVIF"):
raise Exception()
if ".avif" in Image.registered_extensions():
HAVE_AVIF = True
raise Exception()
import pillow_avif # noqa: F401 # pylint: disable=unused-import
HAVE_AVIF = True
@ -101,14 +116,28 @@ except:
try:
if os.environ.get("PRTY_NO_VIPS"):
raise Exception()
raise ImportError()
HAVE_VIPS = True
import pyvips
logging.getLogger("pyvips").setLevel(logging.WARNING)
except:
except Exception as e:
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 = {}
@ -163,12 +192,15 @@ class ThumbSrv(object):
self.mutex = threading.Lock()
self.busy: dict[str, list[threading.Condition]] = {}
self.untemp: dict[str, list[str]] = {}
self.ram: dict[str, float] = {}
self.memcond = threading.Condition(self.mutex)
self.stopping = False
self.rm_nullthumbs = True # forget failed conversions on startup
self.nthr = max(1, self.args.th_mt)
self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(","))
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
for n in range(self.nthr):
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
@ -191,11 +223,19 @@ class ThumbSrv(object):
if self.args.th_clean:
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(","))
for y in [
self.args.th_r_pil,
self.args.th_r_vips,
self.args.th_r_raw,
self.args.th_r_ffi,
self.args.th_r_ffv,
self.args.th_r_ffa,
@ -218,6 +258,9 @@ class ThumbSrv(object):
if "vips" in self.args.th_dec:
self.thumbable |= self.fmt_vips
if "raw" in self.args.th_dec:
self.thumbable |= self.fmt_raw
if "ff" in self.args.th_dec:
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable |= zss
@ -255,7 +298,8 @@ class ThumbSrv(object):
self.log("joined waiting room for %r" % (tpath,))
except:
thdir = os.path.dirname(tpath)
bos.makedirs(os.path.join(thdir, "w"))
chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755
bos.makedirs(os.path.join(thdir, "w"), vf=chmod)
inf_path = os.path.join(thdir, "dir.txt")
if not bos.path.exists(inf_path):
@ -270,7 +314,7 @@ class ThumbSrv(object):
vn = next((x for x in allvols if x.realpath == ptop), None)
if not vn:
self.log("ptop %r not in %s" % (ptop, allvols), 3)
vn = self.asrv.vfs.all_aps[0][1]
vn = self.asrv.vfs.all_aps[0][1][0]
self.q.put((abspath, tpath, fmt, vn))
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)
@ -298,6 +342,7 @@ class ThumbSrv(object):
"thumbable": self.thumbable,
"pil": self.fmt_pil,
"vips": self.fmt_vips,
"raw": self.fmt_raw,
"ffi": self.fmt_ffi,
"ffv": self.fmt_ffv,
"ffa": self.fmt_ffa,
@ -340,8 +385,10 @@ class ThumbSrv(object):
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus
want_au = want_mp3 or want_opus or want_flac or want_wav
for lib in self.args.th_dec:
can_au = lib == "ff" and (
ext in self.fmt_ffa or ext in self.fmt_ffv
@ -351,11 +398,17 @@ class ThumbSrv(object):
funs.append(self.conv_pil)
elif lib == "vips" and ext in self.fmt_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):
if want_opus:
funs.append(self.conv_opus)
elif want_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:
funs.append(self.conv_waves)
png_ok = True
@ -385,8 +438,12 @@ class ThumbSrv(object):
self.log(msg, c)
if getattr(ex, "returncode", 0) != 321:
if fun == funs[-1]:
try:
with open(ttpath, "wb") as _:
pass
except Exception as ex:
t = "failed to create the file [%s]: %r"
self.log(t % (ttpath, ex), 3)
else:
# ffmpeg may spawn empty files on windows
try:
@ -398,14 +455,25 @@ class ThumbSrv(object):
wunlink(self.log, ap_unpk, vn.flags)
try:
wrename(self.log, ttpath, tpath, vn.flags)
except:
atomic_move(self.log, ttpath, tpath, vn.flags)
except Exception as ex:
if not os.path.exists(tpath):
t = "failed to move [%s] to [%s]: %r"
self.log(t % (ttpath, tpath, ex), 3)
pass
untemp = []
with self.mutex:
subs = self.busy[tpath]
del self.busy[tpath]
self.ram.pop(ttpath, None)
untemp = self.untemp.pop(ttpath, None) or []
for ap in untemp:
try:
wunlink(self.log, ap, VF_CAREFUL)
except:
pass
for x in subs:
with x:
@ -444,9 +512,7 @@ class ThumbSrv(object):
return im
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as 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:
@ -474,6 +540,11 @@ class ThumbSrv(object):
im.save(tpath, **args)
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
self.conv_image_pil(im, tpath, fmt, vn)
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
crops = ["centre", "none"]
@ -495,9 +566,53 @@ class ThumbSrv(object):
assert img # type: ignore # !rm
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:
self.wait4ram(0.2, tpath)
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret:
return
@ -508,6 +623,17 @@ class ThumbSrv(object):
dur = ret[".dur"][1] if ".dur" in ret else 4
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="
if "f" in fmt:
scale += "decrease,setsar=1:1"
@ -526,7 +652,7 @@ class ThumbSrv(object):
cmd += seek
cmd += [
b"-i", fsenc(abspath),
b"-map", b"0:v:0",
b"-map", imap,
b"-vf", bscale,
b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0",
@ -547,11 +673,11 @@ class ThumbSrv(object):
]
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"))
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:
return
@ -595,7 +721,7 @@ class ThumbSrv(object):
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
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:
raise Exception("not audio")
@ -633,7 +759,7 @@ class ThumbSrv(object):
# fmt: on
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn)
self._run_ff(cmd, vn, "convt")
if "pngquant" in vn.flags:
wtpath = tpath + ".png"
@ -652,22 +778,70 @@ class ThumbSrv(object):
except:
pass
else:
wrename(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:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
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()
# https://trac.ffmpeg.org/ticket/10797
# expect 1 GiB every 600 seconds when duration is tricky;
# simple filetypes are generally safer so let's special-case those
safe = ("flac", "wav", "aif", "aiff", "opus")
coeff = 1800 if abspath.split(".")[-1].lower() in safe else 600
dur = ret[".dur"][1] if ".dur" in ret else 300
coeff = 1800 if fext in EXTS_SPEC_SAFE else 600
dur = ret[".dur"][1] if ".dur" in ret else 900
need = 0.2 + dur / coeff
self.wait4ram(need, tpath)
infile = abspath
if dur >= 900 or fext in self.exts_spec_unsafe:
with tempfile.NamedTemporaryFile(suffix=".spec.flac", delete=False) as f:
f.write(b"h")
infile = f.name
try:
self.untemp[tpath].append(infile)
except:
self.untemp[tpath] = [infile]
# 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"-ac", b"1",
b"-ar", b"48000",
b"-sample_fmt", b"s16",
b"-t", b"900",
b"-y", fsenc(infile),
]
# fmt: on
self._run_ff(cmd, vn, "convt")
fc = "[0:a:0]aresample=48000{},showspectrumpic=s="
if "3" in fmt:
fc += "1280x1024,crop=1420:1056:70:48[o]"
@ -687,7 +861,7 @@ class ThumbSrv(object):
b"-nostdin",
b"-v", b"error",
b"-hide_banner",
b"-i", fsenc(abspath),
b"-i", fsenc(infile),
b"-filter_complex", fc.encode("utf-8"),
b"-map", b"[o]",
b"-frames:v", b"1",
@ -708,7 +882,7 @@ class ThumbSrv(object):
]
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:
quality = self.args.q_mp3.lower()
@ -716,7 +890,7 @@ class ThumbSrv(object):
raise Exception("disabled in server config")
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:
raise Exception("not audio")
@ -747,14 +921,74 @@ class ThumbSrv(object):
fsenc(tpath)
]
# 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:
if self.args.no_acode or not self.args.q_opus:
raise Exception("disabled in server config")
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:
raise Exception("not audio")
@ -803,7 +1037,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
def _conv_caf(
self,
@ -843,7 +1077,7 @@ class ThumbSrv(object):
fsenc(tmp_opus)
]
# 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
# (average file shorter than 8 seconds), so of course we
@ -870,7 +1104,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
else:
# simple remux should be safe
@ -889,7 +1123,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd, vn, oom=300)
self._run_ff(cmd, vn, "aconvt", oom=300)
try:
wunlink(self.log, tmp_opus, vn.flags)
@ -991,6 +1225,8 @@ class ThumbSrv(object):
# thumb file
try:
b64, ts, ext = f.split(".")
if len(ts) > 8 and PTN_TS.match(ts):
ts = "yeahokay"
if len(b64) != 24 or len(ts) != 8 or ext not in exts:
raise Exception()
except:

View file

@ -134,9 +134,9 @@ class U2idx(object):
assert sqlite3 # type: ignore # !rm
ptop = vn.realpath
histpath = self.asrv.vfs.histtab.get(ptop)
histpath = self.asrv.vfs.dbpaths.get(ptop)
if not histpath:
self.log("no histpath for %r" % (ptop,))
self.log("no dbpath for %r" % (ptop,))
return None
db_path = os.path.join(histpath, "up2k.db")
@ -391,7 +391,7 @@ class U2idx(object):
fk_alg = 2 if "fka" in flags else 1
c = cur.execute(uq, tuple(vuv))
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("//"):
rd, fn = s3dec(rd, fn)

View file

@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals
import errno
import gzip
import hashlib
import json
import math
@ -42,6 +41,7 @@ from .util import (
fsenc,
gen_filekey,
gen_filekey_dbg,
gzip,
hidedir,
humansize,
min_ex,
@ -77,7 +77,7 @@ except:
if HAVE_SQLITE3:
import sqlite3
DB_VER = 5
DB_VER = 6
if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Pattern, Union
@ -86,7 +86,10 @@ if TYPE_CHECKING:
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"
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"
VF_AFFECTS_INDEXING = set(zsg.split(" "))
@ -94,7 +97,7 @@ VF_AFFECTS_INDEXING = set(zsg.split(" "))
SBUSY = "cannot receive uploads right now;\nserver busy with %s.\nPlease wait; the client will retry..."
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume), or, if you want to keep the thumbnails in the current location and only move the database itself, then use --dbpath or volflag dbpath"
NULLSTAT = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0))
@ -141,6 +144,7 @@ class Up2k(object):
self.salt = self.args.warksalt
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
self.abrt_key = ""
self.gid = 0
self.gt0 = 0
@ -372,11 +376,12 @@ class Up2k(object):
if ineed == ihash or not ineed:
continue
poke = job["poke"]
zt = (
ineed / ihash,
job["size"],
int(job["t0c"]),
int(job["poke"]),
int(job.get("t0c", poke)),
int(poke),
djoin(vtop, job["prel"], job["name"]),
)
ret.append(zt)
@ -399,12 +404,14 @@ class Up2k(object):
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):
return '[{"timeout":1}]'
return {"timeout": 1}
ret: list[tuple[int, str, int, int, int]] = []
userset = set([(uname or "\n"), "*"])
n = 1000
try:
for ptop, tab2 in self.registry.items():
cfg = self.flags.get(ptop, {}).get("u2abort", 1)
@ -419,7 +426,6 @@ class Up2k(object):
or (addr and addr != job["addr"])
):
continue
zt5 = (
int(job["t0"]),
djoin(job["vtop"], job["prel"], job["name"]),
@ -428,6 +434,9 @@ class Up2k(object):
len(job["hash"]),
)
ret.append(zt5)
n -= 1
if not n:
break
finally:
self.reg_mutex.release()
@ -444,7 +453,7 @@ class Up2k(object):
}
for (at, vp, sz, nn, nh) in ret
]
return json.dumps(ret2, separators=(",\n", ": "))
return {"f": ret2}
def get_unfinished(self) -> str:
if PY2 or not self.reg_mutex.acquire(timeout=0.5):
@ -557,6 +566,7 @@ class Up2k(object):
else:
# important; not deferred by db_act
timeout = self._check_lifetimes()
timeout = min(self._check_forget_ip(), timeout)
try:
if self.args.shr:
timeout = min(self._check_shares(), timeout)
@ -617,6 +627,43 @@ class Up2k(object):
for v in vols:
volage[v] = now
def _check_forget_ip(self) -> float:
now = time.time()
timeout = now + 9001
for vp, vol in sorted(self.vfs.all_vols.items()):
maxage = vol.flags["forget_ip"]
if not maxage:
continue
cur = self.cur.get(vol.realpath)
if not cur:
continue
cutoff = now - maxage * 60
for _ in range(2):
q = "select ip, at from up where ip > '' order by +at limit 1"
hits = cur.execute(q).fetchall()
if not hits:
break
remains = hits[0][1] - cutoff
if remains > 0:
timeout = min(timeout, now + remains)
break
q = "update up set ip = '' where ip > '' and at <= %d"
cur.execute(q % (cutoff,))
zi = cur.rowcount
cur.connection.commit()
t = "forget-ip(%d) removed %d IPs from db [/%s]"
self.log(t % (maxage, zi, vol.vpath))
timeout = min(timeout, now + 900)
return timeout
def _check_lifetimes(self) -> float:
now = time.time()
timeout = now + 9001
@ -856,7 +903,7 @@ class Up2k(object):
self.iacct = self.asrv.iacct
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())
t0 = time.time()
@ -877,7 +924,8 @@ class Up2k(object):
# only need to protect register_vpath but all in one go feels right
for vol in vols:
try:
bos.makedirs(vol.realpath) # gonna happen at snap anyways
# mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vf=vol.flags)
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
except Exception as ex:
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
@ -1058,9 +1106,9 @@ class Up2k(object):
self, ptop: str, flags: dict[str, Any]
) -> Optional[tuple["sqlite3.Cursor", str]]:
"""mutex(main,reg) me"""
histpath = self.vfs.histtab.get(ptop)
histpath = self.vfs.dbpaths.get(ptop)
if not histpath:
self.log("no histpath for %r" % (ptop,))
self.log("no dbpath for %r" % (ptop,))
return None
db_path = os.path.join(histpath, "up2k.db")
@ -1081,7 +1129,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
zs = "html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot"
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@ -1103,6 +1151,20 @@ class Up2k(object):
del fl[k1]
else:
fl[k1] = ",".join(x for x in fl[k1])
if fl["chmod_d"] == int(self.args.chmod_d, 8):
fl.pop("chmod_d")
try:
if fl["chmod_f"] == int(self.args.chmod_f or "-1", 8):
fl.pop("chmod_f")
except:
pass
for k in ("chmod_f", "chmod_d"):
try:
fl[k] = "%o" % (fl[k])
except:
pass
a = [
(ft if v is True else ff if v is False else fv).format(k, str(v))
for k, v in fl.items()
@ -1306,12 +1368,15 @@ class Up2k(object):
]
excl += [absreal(x) for x in excl]
excl += list(self.vfs.histtab.values())
excl += list(self.vfs.dbpaths.values())
if WINDOWS:
excl = [x.replace("/", "\\") for x in excl]
else:
# ~/.wine/dosdevices/z:/ and such
excl.extend(("/dev", "/proc", "/run", "/sys"))
excl = list({k: 1 for k in excl})
if self.args.re_dirsz:
db.c.execute("delete from ds")
db.n += 1
@ -1323,6 +1388,10 @@ class Up2k(object):
t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem"
self.log(t % (vol.vpath, rtop), 6)
return True, False
if not vol.check_landmarks():
t = "volume /%s at [%s] will not be indexed due to bad landmarks"
self.log(t % (vol.vpath, rtop), 6)
return True, False
n_add, _, _ = self._build_dir(
db,
@ -1414,7 +1483,7 @@ class Up2k(object):
unreg: list[str] = []
files: list[tuple[int, int, str]] = []
fat32 = True
cv = ""
cv = vcv = ""
th_cvd = self.args.th_coversd
th_cvds = self.args.th_coversd_set
@ -1509,25 +1578,24 @@ class Up2k(object):
rsz += sz
files.append((sz, lmod, iname))
if sz:
liname = iname.lower()
ext = liname.rsplit(".", 1)[-1]
if (
sz
and (
liname in th_cvds
or (
not cv
and liname.rsplit(".", 1)[-1] in CV_EXTS
and not iname.startswith(".")
)
)
and (
or (not cv and ext in ICV_EXTS and not iname.startswith("."))
) and (
not cv
or liname not in th_cvds
or cv.lower() not in th_cvds
or th_cvd.index(liname) < th_cvd.index(cv.lower())
)
):
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:
tnf += len(files)
@ -1587,7 +1655,7 @@ class Up2k(object):
abspath = cdirs + fn
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:
c = db.c.execute(sql, (rd, fn))
except:
@ -1596,7 +1664,7 @@ class Up2k(object):
in_db = list(c.fetchall())
if in_db:
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:
t = "WARN: multiple entries: %r => %r |%d|\n%r"
rep_db = "\n".join([repr(x) for x in in_db])
@ -1609,6 +1677,9 @@ class Up2k(object):
if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz):
continue
if un is None:
un = ""
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
self.log(t % (top, rp, dts, lmod, dsz, sz))
self.db_rm(db.c, rd, fn, 0)
@ -1619,6 +1690,7 @@ class Up2k(object):
dw = ""
ip = ""
at = 0
un = ""
self.pp.msg = "a%d %s" % (self.pp.n, abspath)
@ -1644,9 +1716,10 @@ class Up2k(object):
if dw and dw != wark:
ip = ""
at = 0
un = ""
# 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.nf += 1
tfa += 1
@ -2079,11 +2152,12 @@ class Up2k(object):
return -1
w = bw[:-1].decode("ascii")
w16 = w[:16]
with self.mutex:
try:
q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at = cur.execute(q, (w[:16], w)).fetchone()
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone()
except:
# file modified/deleted since spooling
continue
@ -2092,18 +2166,25 @@ class Up2k(object):
rd, fn = s3dec(rd, fn)
if "mtp" in flags:
q = "select 1 from mt where w=? and +k='t:mtp' limit 1"
if cur.execute(q, (w16,)).fetchone():
continue
q = "insert into mt values (?,'t:mtp','a')"
cur.execute(q, (w[:16],))
cur.execute(q, (w16,))
abspath = djoin(ptop, rd, fn)
self.pp.msg = "c%d %s" % (nq, abspath)
if not mpool:
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at)
else:
if ip:
oth_tags = {"up_ip": ip, "up_at": at}
n_tags = self._tagscan_file(cur, entags, w, abspath, ip, at, un)
else:
oth_tags = {}
if ip:
oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
mpool.put(Mpqe({}, entags, w, abspath, oth_tags))
with self.mutex:
@ -2149,7 +2230,7 @@ class Up2k(object):
return tf, -1
if flt == 1:
q = "select w from mt where w = ?"
q = "select 1 from mt where w=? and +k != 't:mtp'"
if c2.execute(q, (row[0][:16],)).fetchone():
continue
@ -2259,8 +2340,8 @@ class Up2k(object):
if w in in_progress:
continue
q = "select rd, fn, ip, at from up where substr(w,1,16)=? limit 1"
rd, fn, ip, at = cur.execute(q, (w,)).fetchone()
q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1"
rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone()
rd, fn = s3dec(rd, fn)
abspath = djoin(ptop, rd, fn)
@ -2284,7 +2365,10 @@ class Up2k(object):
if ip:
oth_tags["up_ip"] = ip
if at:
oth_tags["up_at"] = at
if un:
oth_tags["up_by"] = un
jobs.append(Mpqe(parsers, set(), w, abspath, oth_tags))
in_progress[w] = True
@ -2473,6 +2557,7 @@ class Up2k(object):
abspath: str,
ip: str,
at: float,
un: Optional[str],
) -> int:
"""will mutex(main)"""
assert self.mtag # !rm
@ -2493,7 +2578,10 @@ class Up2k(object):
if ip:
tags["up_ip"] = ip
if at:
tags["up_at"] = at
if un:
tags["up_by"] = un
with self.mutex:
return self._tag_file(write_cur, entags, wark, abspath, tags)
@ -2597,16 +2685,19 @@ class Up2k(object):
if not existed and ver is None:
return self._try_create_db(db_path, cur)
if ver == 4:
for upver in (4, 5):
if ver != upver:
continue
try:
t = "creating backup before upgrade: "
cur = self._backup_db(db_path, cur, ver, t)
self._upgrade_v4(cur)
ver = 5
getattr(self, "_upgrade_v%d" % (upver,))(cur)
ver += 1 # type: ignore
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:
# these no longer serve their intended purpose but they're great as additional sanchks
self._add_dhash_tab(cur)
self._add_xiu_tab(cur)
self._add_cv_tab(cur)
@ -2708,7 +2799,7 @@ class Up2k(object):
idx = r"create index up_w on up(w)"
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_fn on up(fn)",
r"create index up_ip on up(ip)",
@ -2741,6 +2832,15 @@ class Up2k(object):
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:
# v5 -> v5a
try:
@ -2762,7 +2862,7 @@ class Up2k(object):
# v5a -> v5b
# store rd+fn rather than warks to support nohash vols
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
except:
pass
@ -2880,7 +2980,6 @@ class Up2k(object):
if ptop not in self.registry:
raise Pebkac(410, "location unavailable")
cj["name"] = sanitize_fn(cj["name"], "")
cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time()
wark = dwark = self._get_wark(cj)
job = None
@ -2916,9 +3015,14 @@ class Up2k(object):
self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"]
)
if vfs.flags.get("up_ts", "") == "fu" or not cj["lmod"]:
zi = cj["lmod"]
bad_mt = zi <= 0 or zi > 0xAAAAAAAA
if bad_mt or vfs.flags.get("up_ts", "") == "fu":
# force upload time rather than last-modified
cj["lmod"] = int(time.time())
if zi and bad_mt:
t = "ignoring impossible last-modified time from client: %s"
self.log(t % (zi,), 6)
alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = []
for ptop, cur in vols:
@ -2934,7 +3038,7 @@ class Up2k(object):
argv = [dwark[:16], dwark]
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("//"):
dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
@ -3186,14 +3290,16 @@ class Up2k(object):
if hr.get("reloc"):
x = pathmod(self.vfs, dst, vp, hr["reloc"])
if x:
zvfs = vfs
ud1 = (vfs.vpath, job["prel"], job["name"])
pdir, _, job["name"], (vfs, rem) = x
dst = os.path.join(pdir, job["name"])
job["vcfg"] = vfs.flags
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
if zvfs.vpath != vfs.vpath:
job["name"] = sanitize_fn(job["name"], "")
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4))
job["hash"] = cj["hash"]
self.log("xbu reloc1:%d..." % (depth,), 6)
@ -3238,7 +3344,7 @@ class Up2k(object):
reg,
"up2k._get_volsize",
)
bos.makedirs(ap2)
bos.makedirs(ap2, vf=vfs.flags)
vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"])
@ -3335,16 +3441,26 @@ class Up2k(object):
return fname
fp = djoin(fdir, fname)
if job.get("replace") and bos.path.exists(fp):
self.log("replacing existing file at %r" % (fp,))
cur = None
ow = job.get("replace") and bos.path.exists(fp)
if ow and "mt" in str(job["replace"]).lower():
mts = bos.stat(fp).st_mtime
mtc = job["lmod"]
if mtc < mts:
t = "will not overwrite; server %d sec newer than client; %d > %d %r"
self.log(t % (mts - mtc, mts, mtc, fp))
ow = False
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
if ow:
self.log("replacing existing file at %r" % (fp,))
cur = None
st = bos.stat(fp)
try:
vrel = vjoin(job["prel"], fname)
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)
except Exception as ex:
self.log("skipping replace-relink: %r" % (ex,))
@ -3359,8 +3475,13 @@ class Up2k(object):
else:
dip = self.hub.iphash.s(ip)
suffix = "-%.6f-%s" % (ts, dip)
f, ret = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
f, ret = ren_open(
fname,
"wb",
fdir=fdir,
suffix="-%.6f-%s" % (ts, dip),
vf=vf,
)
f.close()
return ret
@ -3373,6 +3494,7 @@ class Up2k(object):
rm: bool = False,
lmod: float = 0,
fsrc: Optional[str] = None,
is_mv: bool = False,
) -> None:
if src == dst or (fsrc and fsrc == dst):
t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)"
@ -3389,7 +3511,9 @@ class Up2k(object):
linked = False
try:
if not flags.get("dedup"):
if "reflink" in flags:
raise Exception("reflink")
if not is_mv and not flags.get("dedup"):
raise Exception("dedup is disabled in config")
lsrc = src
@ -3445,6 +3569,7 @@ class Up2k(object):
linked = True
except Exception as ex:
if str(ex) != "reflink":
self.log("cannot link; creating copy: " + repr(ex))
if bos.path.isfile(src):
csrc = src
@ -3655,8 +3780,9 @@ class Up2k(object):
if self.idx_wark(vflags, *z2):
del self.registry[ptop][wark]
else:
for k in "host tnam busy sprs poke t0c".split():
for k in "host tnam busy sprs poke".split():
del job[k]
job.pop("t0c", None)
job["t0"] = int(job["t0"])
job["hash"] = []
job["done"] = 1
@ -3789,16 +3915,16 @@ class Up2k(object):
db_ip = ""
else:
# plugins may expect this to look like an actual IP
db_ip = "1.1.1.1" if self.args.no_db_ip else ip
db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip
sql = "insert into up values (?,?,?,?,?,?,?)"
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0))
sql = "insert into up values (?,?,?,?,?,?,?,?)"
v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr)
try:
db.execute(sql, v)
except:
assert self.mem_cur # !rm
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)
self.volsize[db] += sz
@ -3890,6 +4016,9 @@ class Up2k(object):
except:
pass
def handle_fs_abrt(self, akey: str) -> None:
self.abrt_key = akey
def handle_rm(
self,
uname: str,
@ -3936,7 +4065,7 @@ class Up2k(object):
vn, rem = vn0.get_dbv(rem0)
ptop = vn.realpath
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 ""
user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None
reg = self.registry.get(ptop, {}) if abrt_cfg else {}
@ -3957,17 +4086,22 @@ class Up2k(object):
if partial:
dip = ip
dat = time.time()
dun = uname
un_cfg = 1
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"
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: "
if not dip:
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)"
elif dat < time.time() - self.args.unpost:
t += "uploaded too long ago"
@ -4056,7 +4190,7 @@ class Up2k(object):
try:
ptop = dbv.realpath
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(
ptop, volpath, cur, wark, True, st.st_size, xlink
)
@ -4099,7 +4233,7 @@ class Up2k(object):
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 + "/"):
raise Pebkac(400, "cp: cannot copy parent into subfolder")
@ -4146,6 +4280,8 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :]
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:
v.connection.commit()
@ -4213,9 +4349,9 @@ class Up2k(object):
self.log(t, 1)
raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs))
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
)
c2 = self.cur.get(dvn.realpath)
@ -4240,7 +4376,7 @@ class Up2k(object):
w,
w,
"",
"",
un or "",
ip or "",
at or 0,
)
@ -4313,7 +4449,7 @@ class Up2k(object):
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 + "/"):
raise Pebkac(400, "mv: cannot move parent into subfolder")
@ -4368,6 +4504,8 @@ class Up2k(object):
dvpf = dvp + svpf[len(svp) :]
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:
v.connection.commit()
@ -4389,7 +4527,10 @@ class Up2k(object):
vp = vjoin(dvp, rem)
try:
dvn, drem = self.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem))
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:
pass
@ -4459,7 +4600,7 @@ class Up2k(object):
is_xvol = svn.realpath != dvn.realpath
bos.makedirs(os.path.dirname(dabs))
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
if is_dirlink:
dlabs = absreal(sabs)
@ -4496,7 +4637,7 @@ class Up2k(object):
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)
has_dupes = False
@ -4530,7 +4671,7 @@ class Up2k(object):
w,
w,
"",
"",
un or "",
ip or "",
at or 0,
)
@ -4548,7 +4689,7 @@ class Up2k(object):
dlink = bos.readlink(sabs)
dlink = os.path.join(os.path.dirname(sabs), dlink)
dlink = bos.path.abspath(dlink)
self._symlink(dlink, dabs, dvn.flags, lmod=ftime)
self._symlink(dlink, dabs, dvn.flags, lmod=ftime, is_mv=True)
wunlink(self.log, sabs, svn.flags)
else:
atomic_move(self.log, sabs, dabs, svn.flags)
@ -4628,15 +4769,16 @@ class Up2k(object):
Optional[str],
Optional[int],
Optional[int],
Optional[str],
str,
Optional[int],
str,
]:
cur = self.cur.get(ptop)
if not cur:
return None, None, None, None, None, None
return None, None, None, None, "", None, ""
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:
c = cur.execute(q, (rd, fn))
except:
@ -4645,9 +4787,9 @@ class Up2k(object):
hit = c.fetchone()
if hit:
wark, ftime, fsize, ip, at = hit
return cur, wark, ftime, fsize, ip, at
return cur, None, None, None, None, None
wark, ftime, fsize, ip, at, un = hit
return cur, wark, ftime, fsize, ip, at, un
return cur, None, None, None, "", None, ""
def _forget_file(
self,
@ -4767,7 +4909,7 @@ class Up2k(object):
flags = self.flags.get(ptop) or {}
atomic_move(self.log, sabs, slabs, flags)
bos.utime(slabs, (int(time.time()), int(mt)), False)
self._symlink(slabs, sabs, flags, False)
self._symlink(slabs, sabs, flags, False, is_mv=True)
full[slabs] = (ptop, rem)
sabs = slabs
@ -4826,7 +4968,9 @@ class Up2k(object):
# (for example a volume with symlinked dupes but no --dedup);
# fsrc=sabs is then a source that currently resolves to copy
self._symlink(dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs)
self._symlink(
dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs, is_mv=True
)
return len(full) + len(links)
@ -4934,13 +5078,15 @@ class Up2k(object):
if hr.get("reloc"):
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
if x:
zvfs = vfs
ud1 = (vfs.vpath, job["prel"], job["name"])
pdir, _, job["name"], (vfs, rem) = x
job["vcfg"] = vf = vfs.flags
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
if zvfs.vpath != vfs.vpath:
job["name"] = sanitize_fn(job["name"], "")
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
self.log("xbu reloc2:%d..." % (depth,), 6)
return self._handle_json(job, depth + 1)
@ -4962,8 +5108,13 @@ class Up2k(object):
else:
dip = self.hub.iphash.s(job["addr"])
suffix = "-%.6f-%s" % (job["t0"], dip)
f, job["tnam"] = ren_open(tnam, "wb", fdir=pdir, suffix=suffix)
f, job["tnam"] = ren_open(
tnam,
"wb",
fdir=pdir,
suffix="-%.6f-%s" % (job["t0"], dip),
vf=vf,
)
try:
abspath = djoin(pdir, job["tnam"])
sprs = job["sprs"]
@ -5044,7 +5195,7 @@ class Up2k(object):
def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None:
now = time.time()
histpath = self.vfs.histtab.get(ptop)
histpath = self.vfs.dbpaths.get(ptop)
if not histpath:
return

View file

@ -31,6 +31,17 @@ from collections import Counter
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from queue import Queue
try:
from zlib_ng import gzip_ng as gzip
from zlib_ng import zlib_ng as zlib
sys.modules["gzip"] = gzip
# sys.modules["zlib"] = zlib
# `- somehow makes tarfile 3% slower with default malloc, and barely faster with mimalloc
except:
import gzip
import zlib
from .__init__ import (
ANYWIN,
EXE,
@ -94,6 +105,7 @@ def _ens(want: str) -> tuple[int, ...]:
# WSAENOTSOCK - no longer a socket
# EUNATCH - can't assign requested address (wifi down)
E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET")
E_SCK_WR = _ens("EPIPE ESHUTDOWN EBADFD")
E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
E_ACCESS = _ens("EACCES WSAEACCES")
@ -103,8 +115,14 @@ IP6ALL = "0:0:0:0:0:0:0:0"
try:
import ctypes
import fcntl
HAVE_FCNTL = True
except:
HAVE_FCNTL = False
try:
import ctypes
import termios
except:
pass
@ -136,6 +154,16 @@ try:
except:
HAVE_PSUTIL = False
try:
if os.environ.get("PRTY_NO_MAGIC") or (
ANYWIN and not os.environ.get("PRTY_FORCE_MAGIC")
):
raise Exception()
import magic
except:
pass
if True: # pylint: disable=using-constant-test
import types
from collections.abc import Callable, Iterable
@ -158,8 +186,6 @@ if True: # pylint: disable=using-constant-test
if TYPE_CHECKING:
import magic
from .authsrv import VFS
from .broker_util import BrokerCli
from .up2k import Up2k
@ -217,7 +243,18 @@ except:
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")
@ -234,6 +271,9 @@ SYMTIME = PY36 and os.utime in os.supports_follow_symlinks
META_NOBOTS = '<meta name="robots" content="noindex, nofollow">\n'
# smart enough to understand javascript while also ignoring rel="nofollow"
BAD_BOTS = r"Barkrowler|bingbot|BLEXBot|Googlebot|GoogleOther|GPTBot|PetalBot|SeekportBot|SemrushBot|YandexBot"
FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z"
URL_PRJ = "https://github.com/9001/copyparty"
@ -359,6 +399,9 @@ application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=v
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 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
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
@ -448,18 +491,22 @@ UNHUMANIZE_UNITS = {
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
FN_EMB = set([".prologue.html", ".epilogue.html", "readme.md", "preadme.md"])
def read_ram() -> tuple[float, float]:
# NOTE: apparently no need to consider /sys/fs/cgroup/memory.max
# (cgroups2) since the limit is synced to /proc/meminfo
a = b = 0
try:
with open("/proc/meminfo", "rb", 0x10000) as f:
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)))
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)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except:
@ -594,6 +641,38 @@ except Exception as ex:
print("using fallback base64 codec due to %r" % (ex,))
class NotUTF8(Exception):
pass
def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool) -> str:
with open(ap, "rb") as f:
buf = f.read()
try:
return buf.decode("utf-8", "strict")
except UnicodeDecodeError as ex:
eo = ex.start
eb = buf[eo : eo + 1]
if not strict:
t = "WARNING: The file [%s] is not using the UTF-8 character encoding; some characters in the file will be skipped/ignored. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
t = t % (ap, eb, eo)
if log:
log(t, 3)
else:
print(t)
return buf.decode("utf-8", "replace")
t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8."
t = t % (ap, eb, eo)
if log:
log(t, 3)
else:
print(t)
raise NotUTF8(t)
class Daemon(threading.Thread):
def __init__(
self,
@ -1200,8 +1279,6 @@ class Magician(object):
self.magic: Optional["magic.Magic"] = None
def ext(self, fpath: str) -> str:
import magic
try:
if self.bad_magic:
raise Exception()
@ -1419,8 +1496,6 @@ def stackmon(fp: str, ival: float, suffix: str) -> None:
buf = st.encode("utf-8", "replace")
if fp.endswith(".gz"):
import gzip
# 2459b 2304b 2241b 2202b 2194b 2191b lv3..8
# 0.06s 0.08s 0.11s 0.13s 0.16s 0.19s
buf = gzip.compress(buf, compresslevel=6)
@ -1500,6 +1575,12 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
if vol.histpath != vol.dbpath:
bdp = vol.dbpath.encode("utf-8")
bdph = b"$db(/" + bvp + b")"
txt = txt.replace(bdp, bdph)
txt = txt.replace(bdp.replace(b"\\", b"\\\\"), bdph)
if txt != txt0:
txt += b"\r\nNOTE: filepaths sanitized; see serverlog for correct values"
@ -1520,6 +1601,8 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None)
vf = kwargs.pop("vf", None)
fperms = vf and "fperms" in vf
if fname == os.devnull:
return fun(fname, *args, **kwargs), fname
@ -1563,6 +1646,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8"))
if fperms:
set_fperms(f2, vf)
if fperms:
set_fperms(f, vf)
return f, fname
@ -1624,14 +1712,10 @@ class MultipartParser(object):
self.args = args
self.headers = http_headers
self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
self.re_cdisp = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
self.re_cdisp_field = re.compile(
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
self.re_cdisp_file = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
self.re_ctype = RE_CTYPE
self.re_cdisp = RE_CDISP
self.re_cdisp_field = RE_CDISP_FIELD
self.re_cdisp_file = RE_CDISP_FILE
self.boundary = b""
self.gen: Optional[
@ -1903,7 +1987,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
return fn
def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
if alg == 1:
zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
else:
@ -1913,6 +1997,13 @@ def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str
return ub64enc(hashlib.sha512(zb).digest()).decode("ascii")
def _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
return _gen_filekey(alg, salt, fspath.replace("/", "\\"), fsize, inode)
gen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey
def gen_filekey_dbg(
alg: int,
salt: str,
@ -1959,15 +2050,25 @@ def formatdate(ts: Optional[float] = None) -> str:
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")
if dur:
exp = formatdate(time.time() + dur)
else:
exp = "Fri, 15 Aug 1997 01:00:00 GMT"
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax"
return t % (k, v, r, exp, "; Secure" if tls else "", txt)
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s"
return t % (
k,
v,
r,
exp,
"; Secure" if tls else "",
txt,
"Lax" if lax else "Strict",
)
def humansize(sz: float, terse: bool = False) -> str:
@ -2156,6 +2257,16 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:
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:
"""html.escape but also newlines"""
s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@ -2288,6 +2399,21 @@ def ujoin(rd: str, fn: str) -> str:
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(
log: "NamedLogger",
re: dict[str, str],
@ -2336,11 +2462,11 @@ def pathmod(
# try to map abspath to vpath
np = np.replace("/", os.sep)
for vn_ap, vn in vfs.all_aps:
for vn_ap, vns in vfs.all_aps:
if not np.startswith(vn_ap):
continue
zs = np[len(vn_ap) :].replace(os.sep, "/")
nvp = vjoin(vn.vpath, zs)
nvp = vjoin(vns[0].vpath, zs)
break
if nvp == "\n":
@ -2475,6 +2601,14 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
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(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool:
@ -2523,6 +2657,11 @@ def _fs_mvrm(
now = time.time()
if ex.errno == errno.ENOENT:
return False
if not attempt and ex.errno == errno.EXDEV:
t = "using copy+delete (%s)\n %s\n %s"
log(t % (ex.strerror, src, dst))
osfun = shutil.move
continue
if now - t0 > maxtime or attempt == 90209:
raise
if not attempt:
@ -2547,15 +2686,18 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
elif flags.get("mv_re_t"):
_fs_mvrm(log, src, dst, True, flags)
else:
try:
os.replace(bsrc, bdst)
def wrename(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> bool:
if not flags.get("mv_re_t"):
os.rename(fsenc(src), fsenc(dst))
return True
return _fs_mvrm(log, src, dst, False, flags)
except OSError as ex:
if ex.errno != errno.EXDEV:
raise
t = "using copy+delete (%s);\n %s\n %s"
log(t % (ex.strerror, src, dst))
try:
os.unlink(bdst)
except:
pass
shutil.move(bsrc, bdst)
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
@ -2566,7 +2708,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
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:
ap = fsenc(abspath)
while prune and not os.path.isdir(ap) and BOS_SEP in ap:
@ -2577,17 +2719,22 @@ def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str
assert ctypes # type: ignore # !rm
abspath = fsdec(ap)
bfree = ctypes.c_ulonglong(0)
btotal = ctypes.c_ulonglong(0)
bavail = ctypes.c_ulonglong(0)
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:
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
return (free, total, "")
except Exception as ex:
return (None, None, repr(ex))
return (0, 0, repr(ex))
if not ANYWIN and not MACOS:
@ -2807,6 +2954,27 @@ def load_ipu(
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]:
readsz = min(bufsz, 128 * 1024)
with open(fsenc(fn), "rb", bufsz) as f:
@ -2838,6 +3006,17 @@ def justcopy(
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(
fin: Generator[bytes, None, None],
fout: Union[typing.BinaryIO, typing.IO[Any]],
@ -3084,11 +3263,13 @@ def unescape_cookie(orig: str) -> str:
return "".join(ret)
def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
def guess_mime(
url: str, path: str = "", fallback: str = "application/octet-stream"
) -> str:
try:
ext = url.rsplit(".", 1)[1].lower()
except:
return fallback
ext = ""
ret = MIMES.get(ext)
@ -3096,6 +3277,16 @@ def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
x = mimetypes.guess_type(url)
ret = "application/{}".format(x[1]) if x[1] else x[0]
if not ret and path:
try:
with open(fsenc(path), "rb", 0) as f:
ret = magic.from_buffer(f.read(4096), mime=True)
if ret.startswith("text/htm"):
# avoid serving up HTML content unless there was actually a .html extension
ret = "text/plain"
except Exception as ex:
pass
if not ret:
ret = fallback
@ -3411,7 +3602,7 @@ def runihook(
verbose: bool,
cmd: str,
vol: "VFS",
ups: list[tuple[str, int, int, str, str, str, int]],
ups: list[tuple[str, int, int, str, str, str, int, str]],
) -> bool:
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
bcmd = [sfsenc(x) for x in acmd]
@ -3888,8 +4079,75 @@ def hidedir(dp) -> None:
pass
_flocks = {}
def _lock_file_noop(ap: str) -> bool:
return True
def _lock_file_ioctl(ap: str) -> bool:
assert fcntl # type: ignore # !rm
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
# NOTE: the fcntl.lockf identifier is (pid,node);
# the lock will be dropped if os.close(os.open(ap))
# is performed anywhere else in this thread
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
_flocks[ap] = fd
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
try:
os.close(fd)
except:
pass
if eno in (errno.EAGAIN, errno.EACCES):
return False
print("WARNING: unexpected errno %d from fcntl.lockf; %r" % (eno, ex))
return True
def _lock_file_windows(ap: str) -> bool:
try:
import msvcrt
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
if eno == errno.EACCES:
return False
print("WARNING: unexpected errno %d from msvcrt.locking; %r" % (eno, ex))
return True
if os.environ.get("PRTY_NO_DB_LOCK"):
lock_file = _lock_file_noop
elif ANYWIN:
lock_file = _lock_file_windows
elif HAVE_FCNTL:
lock_file = _lock_file_ioctl
else:
lock_file = _lock_file_noop
try:
if sys.version_info < (3, 10):
if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"):
# py3.8 doesn't have .files
# py3.9 has broken .is_file
raise ImportError()
@ -3989,7 +4247,12 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(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):
@ -4021,9 +4284,22 @@ class WrongPostKey(Pebkac):
self.datagen = datagen
_: Any = (mp, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER)
_: Any = (
gzip,
mp,
zlib,
BytesIO,
quote,
unquote,
SQLITE_VER,
JINJA_VER,
PYFTPD_VER,
PARTFTPY_VER,
)
__all__ = [
"gzip",
"mp",
"zlib",
"BytesIO",
"quote",
"unquote",

View file

@ -48,6 +48,7 @@ window.baguetteBox = (function () {
var onFSC = function (e) {
isFullscreen = !!document.fullscreenElement;
clmod(document.documentElement, 'bb_fsc', isFullscreen);
};
var overlayClickHandler = function (e) {
@ -402,7 +403,7 @@ window.baguetteBox = (function () {
if (isFullscreen)
document.exitFullscreen();
else
(vid() || ebi('bbox-overlay')).requestFullscreen();
ebi('bbox-overlay').requestFullscreen();
}
catch (ex) {
if (IPHONE)
@ -592,9 +593,7 @@ window.baguetteBox = (function () {
preloadPrev(currentIndex);
});
clmod(ebi('bbox-btns'), 'off');
clmod(btnPrev, 'off');
clmod(btnNext, 'off');
show_buttons(0);
updateOffset();
overlay.style.display = 'block';
@ -776,6 +775,8 @@ window.baguetteBox = (function () {
if (is_vid) {
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
image.setAttribute('controls', 'controls');
image.setAttribute('playsinline', '1');
// ios ignores poster
image.onended = vidEnd;
image.onplay = function () { show_buttons(1); };
image.onpause = function () { show_buttons(); };

File diff suppressed because it is too large Load diff

View file

@ -109,7 +109,7 @@
{%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
{%- 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>
{%- 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

55
copyparty/web/idp.html Normal file
View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ s_doctitle }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#{{ tcolor }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/shares.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
{{ html_head }}
</head>
<body>
<div id="wrap">
<a href="{{ r }}/?idp">refresh</a>
<a href="{{ r }}/?h">control-panel</a>
<table id="tab"><thead><tr>
<th>forget</th>
<th>user</th>
<th>groups</th>
</tr></thead><tbody>
{% for un, gn in rows %}
<tr>
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
<td>{{ un|e }}</td>
<td>{{ gn|e }}</td>
</tr>
{% endfor %}
</tbody></table>
{% if not rows %}
(there are no IdP users in the cache)
{% endif %}
</div>
<a href="#" id="repl">π</a>
<script>
var SR="{{ r }}",
lang="{{ lang }}",
dfavico="{{ favico }}";
var STG = window.localStorage;
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
</script>
<script src="{{ r }}/.cpr/util.js?_={{ ts }}"></script>
{%- if js %}
<script src="{{ js }}_={{ ts }}"></script>
{%- endif %}
</body>
</html>

View file

@ -255,7 +255,7 @@ function Modpoll() {
}
console.log('modpoll...');
var url = (document.location + '').split('?')[0] + '?_=' + Date.now();
var url = (location + '').split('?')[0] + '?_=' + Date.now();
var xhr = new XHR();
xhr.open('GET', url, true);
xhr.responseType = 'text';
@ -346,7 +346,7 @@ function save(e) {
fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt);
var url = (document.location + '').split('?')[0];
var url = (location + '').split('?')[0];
var xhr = new XHR();
xhr.open('POST', url, true);
xhr.responseType = 'text';
@ -404,7 +404,7 @@ function save_cb() {
function run_savechk(lastmod, txt, btn, ntry) {
// 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();
xhr.open('GET', url, true);
xhr.responseType = 'text';
@ -1078,26 +1078,28 @@ action_stack = (function () {
var p1 = from.length,
p2 = to.length;
while (p1-- > 0 && p2-- > 0)
while (p1 --> 0 && p2 --> 0)
if (from[p1] != to[p2])
break;
if (car > ++p1) {
if (car > ++p1)
car = p1;
}
var txt = from.substring(car, p1)
return {
car: car,
cdr: ++p2,
cdr: p2 + (car && 1),
txt: txt,
cpos: cpos
};
}
var undiff = function (from, change) {
var t1 = from.substring(0, change.car),
t2 = from.substring(change.cdr);
return {
txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
txt: t1 + change.txt + t2,
cpos: change.cpos
};
}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
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('hits').innerHTML = 'showing ' + ups.length + ' files';
@ -10,11 +11,12 @@ function render() {
fn = esc(uricom_dec(vsp[1])),
at = f.at,
td = now - f.at,
ts = !at ? '(?)' : unix2iso(at),
ts = !at ? '(?)' : unix2ui(at),
sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'),
sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
html.push('<tr><td>' + sz +
'</td><td>' + (f.un || '') +
'</td><td>' + f.ip +
'</td><td>' + ts +
'</td><td>' + sa +
@ -26,7 +28,8 @@ function render() {
var t = V.filter ? ' matching the filter' : '';
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();
@ -46,7 +49,7 @@ function ask(e) {
V = JSON.parse(this.responseText)
}
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;
}
render();

View file

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

View file

@ -3,7 +3,7 @@ for (var a = 0; a < t.length; a++)
t[a].onclick = 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.open('POST', u, true);
@ -13,7 +13,7 @@ function rm() {
function bump() {
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.open('POST', u, true);
@ -25,7 +25,7 @@ function cb() {
if (this.status !== 200)
return modal.alert('<h6>server error</h6>' + esc(unpre(this.responseText)));
document.location = '?shares';
location = '?shares';
}
function qr(e) {
@ -64,7 +64,7 @@ function showqr(href) {
for (var b = 7; b < 9; b++) {
var v = buf[ibuf++];
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++)

View file

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

View file

@ -15,14 +15,14 @@
<body>
<div id="wrap">
{%- 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>
{%- if this.uname == '*' %}
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>
{%- else %}
<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 %}
@ -120,9 +120,14 @@
<div>
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<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" />
{% endif %}
<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 %}
<a id="x" href="#">change password</a>
{% endif %}
@ -135,6 +140,10 @@
<h1 id="cc">other stuff:</h1>
<ul>
{%- if this.uname in this.args.idp_adm_set %}
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{% endif %}
{%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{% endif %}
@ -159,6 +168,13 @@
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
{%- if this.uname != '*' %}
<li><form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form></li>
{% endif %}
</ul>
</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 = {
"nor": {
"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",
"k1": "nullstill innstillinger",
"l1": "logg inn:",
"ls3": "logg inn",
"lu4": "brukernavn",
"lp4": "passord",
"lo3": "logg ut “{0}” overalt",
"lo2": "avslutter økten på alle nettlesere",
"m1": "velkommen tilbake,",
"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>',
@ -39,17 +46,18 @@ var Ls = {
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplastede filer",
"ag1": "vis kjente IdP-brukere",
},
"eng": {
"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",
"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",
"v2": "use this server as a local HDD",
"ta1": "fill in your new password first",
"ta2": "repeat to confirm new password:",
"ta3": "found a typo; please try again",
},
"chi": {
"a1": "更新",
"b1": "你好 &nbsp; <small>(你尚未登录)</small>",
@ -66,6 +74,11 @@ var Ls = {
"j1": "k304 会在每个 HTTP 304 时断开连接。这有助于避免某些代理服务器卡住或突然停止加载页面,但也会显著降低性能。",
"k1": "重置设置",
"l1": "登录:",
"ls3": "登录", //m
"lu4": "用户名", //m
"lp4": "密码", //m
"lo3": "在所有地方注销 {0}", //m
"lo2": "这将结束在所有浏览器中的会话", //m
"m1": "欢迎回来,",
"n1": "404: 文件不存在 &nbsp;┐( ´ -`)┌",
"o1": '或者你可能没有权限?尝试输入密码或 <a href="' + SR + '/?h">回家</a>',
@ -90,7 +103,662 @@ var Ls = {
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
"ae1": "正在下载:", //m
"af1": "显示最近上传的文件", //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)
@ -109,7 +777,14 @@ for (var k in (d || {})) {
o[a].innerHTML = d[k];
else if (f == 2)
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 {
if (is_idp) {
@ -121,8 +796,8 @@ try {
catch (ex) { }
tt.init();
var o = QS('input[name="cppwd"]');
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus();
o = ebi('u');
@ -131,6 +806,9 @@ if (o && /[0-9]+$/.exec(o.innerHTML))
ebi('uhash').value = '' + location.hash;
if (/\&re=/.test('' + location))
ebi('a').className = 'af g';
(function() {
if (!ebi('x'))
return;

View file

@ -36,12 +36,13 @@
<span class="os lin mac">
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</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>
{% 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>
{% endif %}
@ -101,6 +102,7 @@
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
{%- endif %}
</pre>
<p>on KDE Dolphin, use <code>webdav{{ s }}://{{ ep }}/{{ rvp }}</code></p>
</div>
<div class="os mac">
@ -239,14 +241,26 @@
<div class="os win">
<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">
{ "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
"accept": "url"
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"FileFormName": "f" }

View file

@ -49,12 +49,14 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
ebi('setpw').onclick = function (e) {
var pw = '';
function setpw(e) {
ev(e);
modal.prompt('password:', '', function (v) {
if (!v)
return;
pw = v;
var pw0 = ebi('pw0').innerHTML,
oa = QSA('b');
@ -65,3 +67,14 @@ ebi('setpw').onclick = function (e) {
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

@ -381,6 +381,9 @@ html.y .btn:focus {
box-shadow: 0 .1em .2em #037 inset;
outline: #037 solid .1em;
}
input, button {
font-family: var(--font-main), sans-serif;
}
input[type="submit"] {
cursor: pointer;
}

View file

@ -1,6 +1,18 @@
"use strict";
(function () {
var x = sread('nosubtle');
if (x === '0' || x === '1')
nosubtle = parseInt(x);
if ((nosubtle > 1 && !CHROME && !FIREFOX) ||
(nosubtle > 2 && !CHROME) ||
(CHROME && nosubtle > VCHROME) ||
!WebAssembly)
nosubtle = 0;
})();
function goto_up2k() {
if (up2k === false)
return goto('bup');
@ -23,7 +35,7 @@ var up2k = null,
m = 'will use ' + sha_js + ' instead of native sha512 due to';
try {
if (sread('nosubtle') || window.nosubtle)
if (nosubtle)
throw 'chickenbit';
var cf = crypto.subtle || crypto.webkitSubtle;
cf.digest('SHA-512', new Uint8Array(1)).then(
@ -825,7 +837,7 @@ function up2k_init(subtle) {
}
qsr('#u2depmsg');
var o = mknod('div', 'u2depmsg');
o.innerHTML = m;
o.innerHTML = nosubtle ? '' : m;
ebi('u2foot').appendChild(o);
}
loading_deps = true;
@ -881,10 +893,30 @@ function up2k_init(subtle) {
bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo);
bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null);
bcfg_bind(uc, 'az', 'u2sort', u2sort.indexOf('n') + 1, set_u2sort);
bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME), set_hashw);
bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME || VCHROME > 136), set_hashw);
bcfg_bind(uc, 'hwasm', 'nosubtle', nosubtle, set_nosubtle);
bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);
bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);
uc.ow = parseInt(sread('u2ow', ['0', '1', '2']) || u2ow);
uc.owt = ['🛡️', '🕒', '♻️'];
function set_ow() {
QS('label[for="u2ow"]').innerHTML = uc.owt[uc.ow];
ebi('u2ow').checked = true; //cosmetic
}
ebi('u2ow').onclick = function (e) {
ev(e);
if (++uc.ow > 2)
uc.ow = 0;
swrite('u2ow', uc.ow);
set_ow();
if (uc.ow && !has(perms, 'delete'))
toast.warn(10, L.u_enoow, 'noow');
else if (toast.tag == 'noow')
toast.hide();
};
set_ow();
var st = {
"files": [],
"nfile": {
@ -932,6 +964,7 @@ function up2k_init(subtle) {
"t": 0
},
"car": 0,
"nre": 0,
"slow_io": null,
"oserr": false,
"modn": 0,
@ -1300,7 +1333,7 @@ function up2k_init(subtle) {
if (bad_files.length) {
var msg = L.u_badf.format(bad_files.length, ntot);
for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++)
msg += '-- ' + bad_files[a][1] + '\n';
msg += '-- ' + esc(bad_files[a][1]) + '\n';
msg += L.u_just1;
return modal.alert(msg, function () {
@ -1312,7 +1345,7 @@ function up2k_init(subtle) {
if (nil_files.length) {
var msg = L.u_blankf.format(nil_files.length, ntot);
for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++)
msg += '-- ' + nil_files[a][1] + '\n';
msg += '-- ' + esc(nil_files[a][1]) + '\n';
msg += L.u_just1;
return modal.confirm(msg, function () {
@ -1324,10 +1357,68 @@ function up2k_init(subtle) {
});
}
var fps = new Set(), pdp = '';
for (var a = 0; a < good_files.length; a++) {
var fp = good_files[a][1],
dp = vsplit(fp)[0];
fps.add(fp);
if (pdp != dp) {
pdp = dp;
dp = dp.slice(0, -1);
while (dp) {
fps.add(dp);
dp = vsplit(dp)[0].slice(0, -1);
}
}
}
var junk = [], rmi = [];
for (var a = 0; a < good_files.length; a++) {
var fn = good_files[a][1];
if (fn.indexOf("/.") < 0 && fn.indexOf("/__MACOS") < 0)
continue;
if (/\/__MACOS|\/\.(DS_Store|AppleDouble|LSOverride|DocumentRevisions-|fseventsd|Spotlight-V[0-9]|TemporaryItems|Trashes|VolumeIcon\.icns|com\.apple\.timemachine\.donotpresent|AppleDB|AppleDesktop|apdisk)/.exec(fn)) {
junk.push(good_files[a]);
rmi.push(a);
continue;
}
if (fn.indexOf("/._") + 1 &&
fps.has(fn.replace("/._", "/")) &&
fn.split("/").pop().startsWith("._") &&
!has(rmi, a)
) {
junk.push(good_files[a]);
rmi.push(a);
}
}
if (!junk.length)
return gotallfiles2(good_files);
junk.sort();
rmi.sort(function (a, b) { return a - b; });
var msg = L.u_applef.format(junk.length, good_files.length);
for (var a = 0, aa = Math.min(1000, junk.length); a < aa; a++)
msg += '-- ' + esc(junk[a][1]) + '\n';
return modal.confirm(msg, function () {
for (var a = rmi.length - 1; a >= 0; a--)
good_files.splice(rmi[a], 1);
start_actx();
gotallfiles2(good_files);
}, function () {
start_actx();
gotallfiles2(good_files);
});
}
function gotallfiles2(good_files) {
good_files.sort(function (a, b) {
a = a[1];
b = b[1];
return a < b ? -1 : a > b ? 1 : 0;
return a[1] < b[1] ? -1 : 1;
});
var msg = [];
@ -1338,7 +1429,7 @@ function up2k_init(subtle) {
if (FIREFOX && good_files.length > 3000)
msg.push(L.u_ff_many + "\n\n");
msg.push(L.u_asku.format(good_files.length, esc(get_vpath())) + '<ul>');
msg.push(L.u_asku.format(good_files.length, esc(uricom_dec(get_evpath()))) + '<ul>');
for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++)
msg.push('<li>' + esc(good_files[a][1]) + '</li>');
@ -1365,9 +1456,16 @@ function up2k_init(subtle) {
if (CHROME) {
// chrome-bug 383568268 // #124
nw = Math.max(1, (nw > 4 ? 4 : (nw - 1)));
if (VCHROME < 137)
nw = (subtle && !MOBILE && nw > 2) ? 2 : nw;
}
var x = sread('u2hashers') || window.u2hashers;
if (x) {
console.log('u2hashers is overriding default-value ' + nw);
nw = parseInt(x);
}
for (var a = 0; a < nw; a++)
hws.push(new Worker(SR + '/.cpr/w.hash.js?_=' + TS));
@ -1380,9 +1478,7 @@ function up2k_init(subtle) {
if (!uc.az)
good_files.sort(function (a, b) {
a = a[0].size;
b = b[0].size;
return a < b ? -1 : a > b ? 1 : 0;
return a[0].size - b[0].size;
});
for (var a = 0; a < good_files.length; a++) {
@ -1390,7 +1486,7 @@ function up2k_init(subtle) {
name = good_files[a][1],
fdir = evpath,
now = Date.now(),
lmod = uc.u2ts ? (fobj.lastModified || now) : 0,
lmod = (uc.u2ts && fobj.lastModified) || 0,
ofs = name.lastIndexOf('/') + 1;
if (ofs) {
@ -1477,7 +1573,7 @@ function up2k_init(subtle) {
function linklist() {
var ret = [],
base = document.location.origin.replace(/\/$/, '');
base = location.origin.replace(/\/$/, '');
for (var a = 0; a < st.files.length; a++) {
var t = st.files[a],
@ -1500,7 +1596,7 @@ function up2k_init(subtle) {
ev(e);
var txt = linklist();
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));
});
};
@ -1688,8 +1784,7 @@ function up2k_init(subtle) {
}
var tasker = (function () {
var running = false,
was_busy = false;
var running = false;
var defer = function () {
running = false;
@ -1706,7 +1801,17 @@ function up2k_init(subtle) {
while (true) {
var now = Date.now(),
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)
console.log('main thread blocked for ' + blocktime);
@ -1714,7 +1819,16 @@ function up2k_init(subtle) {
r.tact = now;
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];
if (t.want_recheck) {
t.rechecks++;
@ -1722,7 +1836,7 @@ function up2k_init(subtle) {
push_t(st.todo.handshake, t);
}
}
is_busy = st.todo.handshake.length;
is_busy = !!st.todo.handshake.length;
try {
if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused))
treectl.goto();
@ -1731,7 +1845,7 @@ function up2k_init(subtle) {
}
if (was_busy != is_busy) {
st.is_busy = was_busy = is_busy;
st.is_busy = is_busy;
window[(is_busy ? "add" : "remove") +
"EventListener"]("beforeunload", warn_uploader_busy);
@ -1852,7 +1966,7 @@ function up2k_init(subtle) {
for (var a = 0; a < st.files.length; a++) {
var t = st.files[a];
if (t.want_recheck && !t.rechecks)
if (t.want_recheck && t.rechecks < 999)
return;
}
@ -2054,8 +2168,8 @@ function up2k_init(subtle) {
try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
};
reader.onerror = function () {
var err = reader.error + '';
var handled = false;
var err = esc('' + reader.error),
handled = false;
if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender
err.indexOf('NotFoundError') !== -1 // macos-firefox permissions
@ -2138,6 +2252,7 @@ function up2k_init(subtle) {
reading = 0,
max_readers = 1,
opt_readers = 2,
failed = false,
free = [],
busy = {},
nbusy = 0,
@ -2187,6 +2302,14 @@ function up2k_init(subtle) {
tasker();
}
function go_fail() {
failed = true;
if (nbusy)
return;
apop(st.busy.hash, t);
st.bytes.finished += t.size;
}
function onmsg(d) {
d = d.data;
var k = d[0];
@ -2201,6 +2324,12 @@ function up2k_init(subtle) {
return vis_exh(d[1], 'up2k.js', '', '', d[1]);
if (k == "fail") {
var nchunk = d[1];
free.push(busy[nchunk]);
delete busy[nchunk];
nbusy--;
reading--;
pvis.seth(t.n, 1, d[1]);
pvis.seth(t.n, 2, d[2]);
console.log(d[1], d[2]);
@ -2208,9 +2337,7 @@ function up2k_init(subtle) {
got_oserr();
pvis.move(t.n, 'ng');
apop(st.busy.hash, t);
st.bytes.finished += t.size;
return;
return go_fail();
}
if (k == "ferr")
@ -2243,6 +2370,9 @@ function up2k_init(subtle) {
t.hash.push(nchunk);
pvis.hashed(t);
if (failed)
return go_fail();
if (t.hash.length < nchunks)
return nbusy < opt_readers && go_next();
@ -2279,7 +2409,7 @@ function up2k_init(subtle) {
xhr.onerror = xhr.ontimeout = function () {
console.log('head onerror, retrying', t.name, t);
if (!toast.visible)
toast.warn(9.98, L.u_enethd + "\n\nfile: " + t.name, t);
toast.warn(9.98, L.u_enethd + "\n\nfile: " + esc(t.name), t);
apop(st.busy.head, t);
st.todo.head.unshift(t);
@ -2320,8 +2450,8 @@ function up2k_init(subtle) {
try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
};
xhr.timeout = 34000;
xhr.open('HEAD', t.purl + uricom_enc(t.name), true);
xhr.timeout = 34000;
xhr.send();
}
@ -2354,7 +2484,7 @@ function up2k_init(subtle) {
return console.log('zombie handshake onerror', t.name, t);
if (!toast.visible)
toast.warn(9.98, L.u_eneths + "\n\nfile: " + t.name, t);
toast.warn(9.98, L.u_eneths + "\n\nfile: " + esc(t.name), t);
console.log('handshake onerror, retrying', t.name, t);
apop(st.busy.handshake, t);
@ -2400,8 +2530,8 @@ function up2k_init(subtle) {
var msg = [];
for (var a = 0, aa = Math.min(20, response.hits.length); a < aa; a++) {
var hit = response.hits[a],
tr = unix2iso(hit.ts),
tu = unix2iso(t.lmod),
tr = unix2ui(hit.ts),
tu = unix2ui(t.lmod),
diff = parseInt(t.lmod) - parseInt(hit.ts),
cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b',
sdiff = '<span style="color:#' + cdiff + '">diff ' + diff;
@ -2459,7 +2589,7 @@ function up2k_init(subtle) {
var idx = t.hash.indexOf(missing[a]);
if (idx < 0)
return modal.alert('wtf negative index for hash "{0}" in task:\n{1}'.format(
missing[a], JSON.stringify(t)));
missing[a], esc(JSON.stringify(t))));
t.postlist.push(idx);
cbd[idx] = 0;
@ -2582,8 +2712,9 @@ function up2k_init(subtle) {
if (ofs !== -1) {
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;
if (t.rechecks < 999 && (err_pend || err_srcb)) {
t.want_recheck = true;
if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) {
err = L.u_dupdefer;
@ -2613,7 +2744,7 @@ function up2k_init(subtle) {
return toast.err(0, L.u_ehsdf + "\n\n" + rsp.replace(/.*; /, ''));
err = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit;
xhrchk(xhr, err + "\n\nfile: " + t.name + "\n\nerror ", "404, target folder not found", "warn", t);
xhrchk(xhr, err + "\n\nfile: " + esc(t.name) + "\n\nerror ", "404, target folder not found", "warn", t);
}
}
xhr.onload = function (e) {
@ -2634,6 +2765,13 @@ function up2k_init(subtle) {
else if (t.umod)
req.umod = true;
if (!t.srch) {
if (uc.ow == 1)
req.replace = 'mt';
if (uc.ow == 2)
req.replace = true;
}
xhr.open('POST', t.purl, true);
xhr.responseType = 'text';
xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 :
@ -2693,7 +2831,7 @@ function up2k_init(subtle) {
if (!t.t_uploading)
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),
car = pcar * chunksize,
@ -2763,7 +2901,7 @@ function up2k_init(subtle) {
toast.inf(10, L.u_cbusy);
}
else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)", "warn", t);
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
chill(t);
}
orz2(xhr);
@ -2793,6 +2931,7 @@ function up2k_init(subtle) {
st.bytes.inflight += db;
xhr.bsent = nb;
if (!IE)
xhr.timeout = 64000 + Date.now() - xhr.t0;
pvis.prog(t, pcar, nb);
};
@ -2807,7 +2946,7 @@ function up2k_init(subtle) {
xhr.bsent = 0;
if (!toast.visible)
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), t.name), t);
toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t);
t.nojoin = t.nojoin || t.postlist.length; // maybe rproxy postsize limit
console.log('chunkpit onerror,', t.name, t);
@ -2841,7 +2980,7 @@ function up2k_init(subtle) {
xhr.bsent = 0;
xhr.t0 = Date.now();
xhr.timeout = 42000;
xhr.timeout = 1000 * (IE ? 1234 : 42);
xhr.responseType = 'text';
xhr.send(t.fobj.slice(car, cdr));
}
@ -3068,7 +3207,7 @@ function up2k_init(subtle) {
return;
try {
ebi('lifew').innerHTML = unix2iso((st.lifetime || lifetime) +
ebi('lifew').innerHTML = unix2ui((st.lifetime || lifetime) +
Date.now() / 1000 - new Date().getTimezoneOffset() * 60
).replace(' ', ', ').slice(0, -3);
}
@ -3187,6 +3326,12 @@ function up2k_init(subtle) {
}
}
function set_nosubtle(v) {
if (!WebAssembly)
return toast.err(10, L.u_nowork);
modal.confirm(L.lang_set, location.reload.bind(location), null);
}
function set_upnag(en) {
function nopenag() {
bcfg_set('upnag', uc.upnag = false);

View file

@ -32,7 +32,7 @@ var wah = '',
CHROME = !!window.chrome, // safari=false
VCHROME = CHROME ? 1 : 0,
UA = '' + navigator.userAgent,
IE = /Trident\//.test(UA),
IE = !!document.documentMode,
FIREFOX = ('netscape' in window) && / rv:/.test(UA),
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(UA),
LINUX = /Linux/.test(UA),
@ -69,7 +69,7 @@ try {
CHROME = navigator.userAgentData.brands.find(function (d) { return d.brand == 'Chromium' });
if (CHROME)
VCHROME = CHROME.version;
VCHROME = parseInt(CHROME.version);
else
VCHROME = 0;
@ -120,7 +120,7 @@ function esc(txt) {
function basenames(txt) {
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) {
var err = e.reason;
try {
@ -180,10 +180,13 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
if (!/\.js($|\?)/.exec(url))
return; // chrome debugger
if (url.indexOf('extension://') + 1)
return;
if (url.indexOf(' > eval') + 1 && !evalex_fatal)
return; // md timer
if (IE && url.indexOf('prism.js') + 1)
if (url.indexOf('prism.js') + 1)
return;
if (url.indexOf('easymde.js') + 1)
@ -364,7 +367,8 @@ if (!Element.prototype.matches)
Element.prototype.mozMatchesSelector ||
Element.prototype.webkitMatchesSelector;
if (!Element.prototype.closest)
var CLOSEST = !!Element.prototype.closest;
if (!CLOSEST)
Element.prototype.closest = function (s) {
var el = this;
do {
@ -382,8 +386,10 @@ if (!String.prototype.format)
});
};
var have_URL = false;
try {
new URL('/a/', 'https://a.com/');
have_URL = true;
}
catch (ex) {
console.log('ie11 shim URL()');
@ -461,6 +467,13 @@ function namesan(txt, win, fslash) {
}
var NATSORT, ENATSORT;
try {
NATSORT = new Intl.Collator([], {numeric: true});
}
catch (ex) { }
var crctab = (function () {
var c, tab = [];
for (var n = 0; n < 256; n++) {
@ -614,6 +627,33 @@ function showsort(tab) {
}
}
}
function st_cmp_num(a, b) {
a = a[0];
b = b[0];
return (
a === null ? -1 :
b === null ? 1 :
(a - b)
);
}
function st_cmp_nat(a, b) {
a = a[0];
b = b[0];
return (
a === null ? -1 :
b === null ? 1 :
NATSORT.compare(a, b)
);
}
function st_cmp_gen(a, b) {
a = a[0];
b = b[0];
return (
a === null ? -1 :
b === null ? 1 :
a.localeCompare(b)
);
}
function sortTable(table, col, cb) {
var tb = table.tBodies[0],
th = table.tHead.rows[0].cells,
@ -659,19 +699,17 @@ function sortTable(table, col, cb) {
}
vl.push([v, a]);
}
vl.sort(function (a, b) {
a = a[0];
b = b[0];
if (a === null)
return -1;
if (b === null)
return 1;
if (stype == 'int') {
return reverse * (a - b);
}
return reverse * (a.localeCompare(b));
});
if (stype == 'int')
vl.sort(st_cmp_num);
else if (ENATSORT)
vl.sort(st_cmp_nat);
else
vl.sort(st_cmp_gen);
if (reverse < 0)
vl.reverse();
if (sread('dir1st') !== '0') {
var r1 = [], r2 = [];
for (var i = 0; i < tr.length; i++) {
@ -699,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) {
var ret = [],
apath = '/',
@ -845,7 +893,7 @@ function uricom_adec(arr, li) {
function get_evpath() {
var ret = document.location.pathname;
var ret = location.pathname;
if (ret.indexOf('/') !== 0)
ret = '/' + ret;
@ -857,21 +905,34 @@ function get_evpath() {
}
function get_vpath() {
return uricom_dec(get_evpath());
}
function noq_href(el) {
return el.getAttribute('href').split('?')[0];
}
function pad2(v) {
return ('0' + v).slice(-2);
}
function unix2iso(ts) {
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) {
s = Math.floor(s);
var m = Math.floor(s / 60);
@ -959,9 +1020,13 @@ function lhumantime(v) {
if (!L || tp.length < 2 || tp[1].indexOf('$') + 1)
return t;
var ret = '';
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;
var u, n, ret = '';
for (var a = 0; a < tp.length; a += 2) {
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);
}
@ -1163,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) {
console.log("h-push " + url);
try {
@ -1181,27 +1253,30 @@ function hist_replace(url) {
function sethash(hv) {
if (window.history && history.replaceState) {
hist_replace(document.location.pathname + document.location.search + '#' + hv);
hist_replace(location.pathname + location.search + '#' + hv);
}
else {
document.location.hash = hv;
location.hash = hv;
}
}
function dl_file(url) {
console.log('DL [%s]', url);
var o = mknod('a');
qsr('#dlfth');
var o = mknod('a', 'dlfth');
o.setAttribute('href', url);
o.setAttribute('download', '');
o.click();
document.body.appendChild(o);
ebi('dlfth').click();
qsr('#dlfth');
}
function cliptxt(txt, ok) {
var fb = function () {
console.log('clip-fb');
var o = mknod('input');
var o = mknod('textarea');
o.value = txt;
document.body.appendChild(o);
o.focus();
@ -1211,6 +1286,8 @@ function cliptxt(txt, ok) {
ok();
};
try {
if (!window.isSecureContext)
throw 1;
navigator.clipboard.writeText(txt).then(ok, fb);
}
catch (ex) { fb(); }

View file

@ -4,6 +4,16 @@
function hex2u8(txt) {
return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); }));
}
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
return {
'&': '&amp;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;'
}[c];
});
}
var subtle = null;
@ -19,6 +29,8 @@ catch (ex) {
}
function load_fb() {
subtle = null;
if (self.hashwasm)
return;
importScripts('deps/sha512.hw.js');
console.log('using fallback hasher');
}
@ -64,7 +76,7 @@ onmessage = (d) => {
};
reader.onerror = function () {
busy = false;
var err = reader.error + '';
var err = esc('' + reader.error);
if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender
err.indexOf('NotFoundError') !== -1 // macos-firefox permissions

View file

@ -1,3 +1,822 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 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
## 🧪 new features
* #181 the default chmod (unix-permissions) of new files and folders can now be changed 9921c43e
* `--chmod-d` or volflag `chmod_d` sets directory permissions; default is 755
* `--chmod-f` or volflag `chmod_f` sets file permissions; default is usually 644 (OS-defined)
* see `--help-chmod` which explains the numbers
## 🩹 bugfixes
* #179 couldn't combine `--shr` (shares) and `--xvol` (symlink-guard) 0f0f8d90
* #180 gallery buttons could still be clicked when faded-out 8c32b0e7
* rss-feeds were slightly busted when combined with rp-loc (location-based proxying) 56d3bcf5
* music-playback within search-results no longer jumps into the next folder at end-of-list 9bc4c5d2
* video-playback on iOS now behaves like on all other platforms 78605d9a
* (it would force-switch into fullscreen because that's their default)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0707-1419 `v1.18.2` idp-vol persistence
## 🧪 new features
* IdP-volumes can optionally be persisted across restarts d162502c
* there is a UI to manage the cached users/groups 4f264a0a
* only available to users listed in the new option `--idp-adm`
* api for manually rescanning several volumes at once 42c199e7
* `/some/path/?scan` does that one volume like before
* `/any/path/?scan=/vol1,/another/vol2` rescans `/vol1` and `/another/vol2`
* volflag to hide volume from listing in controlpanel fd7c71d6
## 🩹 bugfixes
* macos: fix confusing crash when blocked by [Little Snitch](https://www.obdev.at/products/littlesnitch/) bf11b2a4
* unpost could break in some hairy reverseproxy setups 1b2d3985
* copyparty32.exe: fix segfault on win7 c9fafb20
* ui: fix navpane overlapping the scrollbar (still a bit jank but eh) 7ef6fd13
* usb-eject: support all volume names ed908b98
* docker: ensure clean slate deb6711b
* fix up2k on ie11 d2714434
## 🔧 other changes
* update buildscript for keyfinder to support llvm 65c4e035
* #175 add `python-magic` into the `iv` and `dj` docker flavors (thx @Morganamilo) 77274e9d
* properly killed the experimental docker flavors to avoid confusion 8306e3d9
* copyparty.exe: updated pillow 299cff3f f6be3905
* avif support was removed to save 2 MiB
## 🌠 fun facts
* this release was slightly delayed due to a [norwegian traffic jam](https://a.ocv.me/pub/g/2025/07/PXL_20250706_143558381.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0622-0020 `v1.18.0` Logtail
## 🧪 new features
* textfile-viewer can now livestream logfiles (and other growing files) 17fa4906 77df17d1 a1c7a095 6ecf4fdc
* see [readme](https://github.com/9001/copyparty/#textfile-viewer) and the [live demo](https://a.ocv.me/pub/demo/logtail/)
* IdP-volumes: extend syntax for excluding certain users/groups 2e53f797
* the commit-message explains it well enough
* new option `--see-dots` to show dotfiles in the web-ui by default c599e2aa
* #171 automatic mimetype detection for files without extensions (thx @Morganamilo!) ec05f8cc 9dd5dec0
* default-disabled since it has a performance impact on webdav
* there are plans to fix this by using the db instead
* #170 improve custom filetype icons
* be less strict; if a thumbnail is set for `.gz` files, use it for `.tar.gz` too c75b0c25
* improve config docs fa5845ff
## 🩹 bugfixes
* cosmetic: get rid of some noise along the bottom of some cards in the gridview 8cae7a71
* cosmetic: satisfy a new syntax warning in cpython-3.14 5ac38648
## 🔧 other changes
* properly document how to [build from source](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch) / build from scratch f61511d8
* update deps
* copyparty.exe: python 3.13 1eff87c3
* webdeps: dompurify 7eca90cc
## 🌠 fun facts
* this release was cooked up in a [swedish forest cabin](https://a.ocv.me/pub/g/nerd-stuff/forestparty.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0527-1939 `v1.17.2` pushing chrome to the limits (and then some)
## 🧪 new features
* not this time
## 🩹 bugfixes
* up2k: improve file-hashing speed on recent versions of google chrome e3e51fb8
* speed increased from 319 to 513 MiB/s by default (but older chrome versions did 748...)
* read the commit message for the full story, but basically chrome has gotten gradually slower over the past couple versions (starting from v133) and this makes it slightly less bad again
* hashing speed can be further improved from `0.5` to `1.1` GiB/s by enabling the `[wasm]` option in the `[⚙️] settings` tab
* this option can be made default-enabled with `--nosubtle 137` but beware that this increases the chances of running into browser-bugs (foreshadowing...)
* up2k: fix errorhandler for browser-bugs (oom and such) 49c71247
* because [chrome-bug 383568268](https://issues.chromium.org/issues/383568268) is about to make a [surprise return?!](https://issues.chromium.org/issues/383568268#comment14)
* #168 fix uploading into shares if path-based proxying is used 9cb93ae1
* #165 unconditionally heed `--rp-loc` 84f5f417
* the config-option for [path-based proxying](https://github.com/9001/copyparty/#reverse-proxy) was ignored if the reverse-proxy was untrusted; this was confusing and not strictly necessary
## 🔧 other changes
* #166 the nixos module was improved once more (thx @msfjarvis!) 48470f6b 60fb1207
* added usage instructions to [minimal-up2k.js](https://github.com/9001/copyparty/tree/hovudstraum/contrib/plugins#example-browser-js), the up2k-ui [simplifier](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) 1d308eeb
* docker: improve feedback if config is bad or missing 28b63e58
## 🌠 fun facts
* this release was tested using an [unreliable rdp connection](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250526_021207825.jpg) through two ssh-jumphosts to a qemu win10 vm back home from the bergen-oslo night train wifi
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0518-2234 `v1.17.1` as seen on archlinux
## 🧪 new features
* new toolbar button to zip/tar the currently open folder 256dad8c
* new options to specify the default checksum algorithm for PUT/bup/WebDAV uploads 0de09860
* #164 new option `--put-name` to specify the filename of nameless uploads 5dcd88a6
* the default is still `put-TIMESTAMP-IPADDRESS.bin`
## 🩹 bugfixes
* #162 password-protected shares was incompatible with password-hashing c3ef3fdc
* #161 m3u playlist creation was only possible over https 94352f27
* when relocating/redirecting an upload from an xbu hook (execute-before-upload), could miss an already existing file at the destination and create another copy 0a9a8077
* some edgecases when moving files between filesystems f425ff51
* improve tagscan-resume after a server restart (primarily for dupes) 41fa6b25
* support prehistoric timestamps in fat16 vhd-drives on windows 261236e3
## 🔧 other changes
* #159 the nixos module was improved (thx @gabevenberg and @chinponya!) d1bca1f5
* an archlinux maintainer adopted the aur package; copyparty is now [officially in arch](https://archlinux.org/packages/extra/any/copyparty/) b9ba783c
* #162 add KDE Dolphin instructions to the conect-page d4a8071d
* audioplayer now knows that `.oga` means `.ogg`
## 🌠 fun facts
* this release contains code [pair-programmed during an anime rave](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250503_222654610.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0426-2149 `v1.17.0` mixtape.m3u
## 🧪 new features
* [m3u playlists](https://github.com/9001/copyparty/#playlists) 897f9d32 ad200f2b 4195762d fff45552
* create and play m3u / m3u8 files
## 🩹 bugfixes
* improve support for ie11 (yes, internet explorer 11) 3090c748 95157d02
* now possible to launch the password-hasher cli while another instance is running dbfc899d
* in preparation of #157 / #159
## 🔧 other changes
* make better decisions when running in a VM with less than 1 GiB RAM dc3b7a27
## 🌠 fun facts
* this release contains code written [less than 1masl](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250425_170037812.jpg) and was gonna be named [hash again](https://www.youtube.com/watch?v=twUFbqyul_M) since it was originally just the password-hasher fix, but then kipun suggested adding playlist support (thx kipun)
* [donations](https://github.com/9001/) are now also possible through github -- good alternative to paypal (y)
* and thanks a lot for the support (and kind words therein) so far, appreciate it :>
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0420-1836 `v1.16.21` unzip-compat
a couple guys have been asking if I accept donations -- thanks a lot!! added a few options on [my github page](https://github.com/9001/) :>
## 🧪 new features
* #156 add button to loop/repeat music 71c55659
## 🩹 bugfixes
* #155 download-as-zip: increase compatibility with the unix `unzip` command db33d68d
* this unfortunately reduces support for huge zipfiles on old software (WinXP and such)
* and makes it less safe to stream zips into unzippers, so use tar.gz instead
* and is perhaps not even a copyparty bug; see commit-message for the full story
## 🔧 other changes
* show warning on Ctrl-A in lazy-loaded folders 5b3a5fe7
* docker: hide keepalive pings from logs d5a9bd80
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0413-2151 `v1.16.20` all sorted
## 🧪 new features
* when enabled, natural-sort will now also apply to tags, not just filenames 7b2bd6da
## 🩹 bugfixes
* some sorting-related stuff 7b2bd6da
* folders with non-ascii names would sort incorrectly in the navpane/sidebar
* natural-sort didn't apply correctly after changing the sort order
* workaround [ffmpeg-bug 10797](https://trac.ffmpeg.org/ticket/10797) 98dcaee2
* reduces ram usage from 1534 to 230 MiB when generating spectrograms of s3xmodit songs (amiga chiptunes)
* disable mdns if only listening on uds (unix-sockets) ffc16109 361aebf8
## 🔧 other changes
* hotkey CTRL-A will now select all files in gridview 233075ae
* and it toggles (just like in list-view) so try pressing it again
* copyparty.exe: upgrade to pillow v11.2.1 c7aa1a35
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0408-2132 `v1.16.19` GHOST
did you know that every song named `GHOST` is a banger? it's true! [ghost](https://www.youtube.com/watch?v=NoUAwC4yiAw) // [ghost](https://www.youtube.com/watch?v=IKKar5SS29E) // [ghost](https://www.youtube.com/watch?v=tFSFlgm_tsw)
## 🧪 new features
* option to store markdown backups out-of-volume fc883418
* the default is still a subfolder named `.hist` next to the markdown file
* `--md-hist v` puts them in the volume's hist-folder instead
* `--md-hist n` disables markdown-backups entirely
* #149 option to store the volume sqlite databases at a custom locations outside the hist-folder e1b9ac63
* new option `--dbpath` works like `--hist` but it only moves the database file, not the thumbnails
* they can be combined, in which case `--hist` is applied to thumbnails, `--dbpath` to the db
* useful when you're squeezing every last drop of performance out of your filesystem (see the issue)
* actively prevent sharing certain databases (sessions/shares) between multiple copyparty instances acfaacbd
* an errormessage was added to explain some different alternatives for doing this safely
* for example by setting `XDG_CONFIG_HOME` which now works on all platforms b17ccc38
## 🩹 bugfixes
* #151 mkdir did not work in locations outside the volume root (via symlinks) 2b50fc20
* improve the ui feedback when trying to play an audio file which failed to transcode f9954bc4
* also helps with server-filesystem issues, including image-thumbs
## 🔧 other changes
* #152 custom fonts are also applied to textboxes and buttons (thx @thaddeuskkr) d450f615
* be more careful with the shares-db 8e0364ef
* be less careful with the sessions-db 8e0364ef
* update deps c0becc64
* web: dompurify
* copyparty.exe: python 3.12.10
* rephrase `-j0` warning on windows to also mention that Microsoft Defender will freak out c0becc64
* #149 add [a script](https://github.com/9001/copyparty/tree/hovudstraum/contrib#zfs-tunepy) to optimize the sqlite databases for storage on zfs 4f397b9b
* block `GoogleOther` (another recalcitrant bot) from zip-downloads c2034f7b
* rephrase `-j0` warning on windows to also mention that Microsoft Defender will freak out c0becc64
* update [contributing.md](https://github.com/9001/copyparty/blob/hovudstraum/CONTRIBUTING.md) with a section regarding LLM/AI-written code cec3bee0
* the [helptext](https://ocv.me/copyparty/helptext.html) will also be uploaded to each github release from now on, [permalink](https://github.com/9001/copyparty/releases/latest/download/helptext.html)
* add review from ixbt forums b383c08c
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0323-2216 `v1.16.18` zlib-ng
## 🧪 new features
* prefer zlib-ng when available 57a56073
* download-as-tar-gz becomes 2.5x faster
* default-enabled in docker-images
* not enabled in copyparty.exe yet; coming in a future python version
* docker: add mimalloc (optional, default-disabled) de2c9788
* gives twice the speed, and twice the ram usage
## 🩹 bugfixes
* small up2k glitch 3c90cec0
## 🔧 other changes
* rename logues/readmes when uploaded with write-only access 2525d594
* since they are used as helptext when viewing the page
* try to block google and other bad bots from `?doc` and `?zip` 99f63adf
* apparently `rel="nofollow"` means nothing these days
### the docker images for this release were built from e1dea7ef
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0316-2002 `v1.16.17` boot2party
## NEW: make it a bootable usb flashdrive
get the party going anywhere, anytime, no OS required! [download flashdrive image](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) or watch the [low-effort demo video](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/hub-demo-hq.webm) which eventually gets to the copyparty part after showing off a bunch of other stuff on there
* there is [source code](https://github.com/9001/asm/tree/hovudstraum/p/hub) and [build instructions](https://github.com/9001/asm/tree/hovudstraum/p/hub/sm/how2build) too
* please don't take this too seriously
## 🧪 new features
* option to specify max-size for download-as-zip/tar 494179bd 0a33336d
* either the total download size (`--zipmaxs 500M`), and/or max number of files (`--zipmaxn 9k`)
* applies to all uesrs by default; can also ignore limits for authorized users (`--zipmaxu`)
* errormessage can be customized with `--zipmaxt "winter is coming... but this download isn't"`
* [appledoubles](https://a.ocv.me/pub/stuff/?doc=appledoubles-and-friends.txt) are detected and skipped when uploading with the browser-UI 78208405
* IdP-volumes can be filtered by group 9c2c4237
* `[/users/${u}]` in a config-file creates the volume for all users like before
* `[/users/${u%+canwrite}]` only if the user is in the `canwrite` group
* `[/users/${u%-admins}]` only if the user is NOT in the `admins` group
## 🩹 bugfixes
* when moving a folder with symlinks, don't expand them into full files 5ab09769
* absolute symlinks are moved as-is; relative symlinks are rewritten so they still point to the same file when possible (if both source and destination are indexed in the db)
* the previous behavior was good for un-deduplicating files after changing the server-settings, but was too inconvenient for all other usecases
* #146 fix downloading from shares when `-j0` enabled 8417098c
* only show the download-as-zip link when the user is actually allowed to 14bb2999
* the suggestions in the serverlog regarding how to fix incorrect X-Forwarded-For settings would be incorrect if the reverse-proxy used IPv6 to communicate with copyparty 16462ee5
* set nofollow on `?doc` links so crawlers don't download binary files as text 6a2644fe
## 🔧 other changes
* #147 IdP: fix the warning about dangerous misconfigurations to be more accurate 29a17ae2
* #143 print a warning on incorrect character-encoding in textfiles (config-files, logues, readmes etc.) 25974d66
* copyparty.exe: update to jinja 3.1.6 (copyparty was *not affected* by the jinja-3.1.5 vuln)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0228-1846 `v1.16.16` lemon melon cookie
<img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
webdev is [like a lemon](https://youtu.be/HPURbfKb7to) sometimes
* read-only demo server at https://a.ocv.me/pub/demo/
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) [client testbed](https://cd.ocv.me/b/)
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2025-02-25)
## recent important news
* [v1.16.15 (2025-02-25)](https://github.com/9001/copyparty/releases/tag/v1.16.15) fixed low-severity xss when uploading maliciously-named files
* [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
* #142 workaround android-chrome timestamp bug 5e12abbb
* all files were uploaded with last-modified year 1601 in specific recent versions of chrome
* https://issues.chromium.org/issues/393149335 has the actual fix; will be out soon
## 🩹 bugfixes
* add helptext for volflags `dk`, `dks`, `dky` 65a7706f
* fix false-positive warning when disabling a global option per-volume by unsetting the volflag
## 🔧 other changes
* #140 nixos: @daimond113 fixed a warning in the nixpkg (thx!) e0fe2b97
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0225-0017 `v1.16.15` fix low-severity vuln
<img src="https://github.com/9001/copyparty/raw/hovudstraum/docs/logo.svg" width="250" align="right"/>
* read-only demo server at https://a.ocv.me/pub/demo/
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) [client testbed](https://cd.ocv.me/b/)
## ⚠️ this fixes a minor vulnerability; CVE-score `3.6`/`10`
[GHSA-m2jw-cj8v-937r](https://github.com/9001/copyparty/security/advisories/GHSA-m2jw-cj8v-937r) aka [CVE-2025-27145](https://www.cve.org/CVERecord?id=CVE-2025-27145) could let an attacker run arbitrary javascript by tricking an authenticated user into uploading files with malicious filenames
* ...but it required some clever social engineering, and is **not likely** to be a cause for concern... ah, better safe than sorry
there is a [discord server](https://discord.gg/25J8CdTT6G) with an `@everyone` in case of future important updates, such as [vulnerabilities](https://github.com/9001/copyparty/security) (most recently 2025-02-25)
## recent important news
* [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
* nothing this time
## 🩹 bugfixes
* fix [GHSA-m2jw-cj8v-937r](https://github.com/9001/copyparty/security/advisories/GHSA-m2jw-cj8v-937r) / [CVE-2025-27145](https://www.cve.org/CVERecord?id=CVE-2025-27145) in 438ea6cc
* when trying to upload an empty files by dragging it into the browser, the filename would be rendered as HTML, allowing javascript injection if the filename was malicious
* issue discovered and reported by @JayPatel48 (thx!)
* related issues in errorhandling of uploads 499ae1c7 36866f1d
* these all had the same consequences as the GHSA above, but a network outage was necessary to trigger them
* which would probably have the lucky side-effect of blocking the javascript download, nice
* paranoid fixing of probably-not-even-issues 3adbb2ff
* fix some markdown / texteditor bugs 407531bc
* only indicate file-versions for markdown files in listings, since it's tricky to edit non-textfiles otherwise
* CTRL-C followed by CTRL-V and CTRL-Z in a single-line file would make a character fall off
* ensure safety of extensions
## 🔧 other changes
* readme:
* mention support for running the server on risc-v 6d102fc8
* mention that the [sony psp](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) can browse and upload 598a29a7
----
# 💾 what to download?
| download link | is it good? | description |
| -- | -- | -- |
| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |
| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |
| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) | ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |
| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.16.14/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |
| [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) | ⚠️ acceptable | similar to the regular sfx, [mostly worse](https://github.com/9001/copyparty#zipapp) |
| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |
| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.16.5/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |
* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.16.14/u2c.exe), all of the options above are mostly equivalent
* the zip and tar.gz files below are just source code
* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0219-2309 `v1.16.14` overwrite by upload
## 🧪 new features
* #139 overwrite existing files by uploading over them e9f78ea7
* default-disabled; a new togglebutton in the upload-UI configures it
* can optionally compare last-modified-time and only overwrite older files
* [GDPR compliance](https://github.com/9001/copyparty#GDPR-compliance) (maybe/probably) 4be0d426
## 🩹 bugfixes
* some cosmetic volflag stuff, all harmless b190e676
* disabling a volflag `foo` with `-foo` shows a warning that `-foo` was not a recognized volflag, but it still does the right thing
* some volflags give the *"unrecognized volflag, will ignore"* warning, but not to worry, they still work just fine:
* `xz` to allow serverside xz-compression of uploaded files
* the option to customize the loader-spinner would glitch out during the initial page load 7d7d5d6c
## 🔧 other changes
* [randpic.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/randpic.py), new 404-handler example, returns a random pic from a folder 60d5f271
* readme: [howto permanent cloudflare tunnel](https://github.com/9001/copyparty#permanent-cloudflare-tunnel) for easy hosting from home 2beb2acc
* [synology-dsm](https://github.com/9001/copyparty/blob/hovudstraum/docs/synology-dsm.md): mention how to update the docker image 56ce5919
* spinner improvements 6858cb06
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0213-2057 `v1.16.13` configure with confidence
## 🧪 new features
* make the config-parser more helpful regarding volflags a255db70
* if an unrecognized volflag is specified, print a warning instead of silently ignoring it
* understand volflag-names with Uppercase and/or kebab-case (dashes), and not just snake_case (underscores)
* improve `--help-flags` to mention and explain all available flags
* #136 WebDAV: support COPY 62ee7f69
* also support overwrite of existing target files (default-enabled according to the spec)
* the user must have the delete-permission to actually replace files
* option to specify custom icons for certain file extensions 7e4702cf
* see `--ext-th` mentioned briefly in the [thumbnails section](https://github.com/9001/copyparty/#thumbnails)
* option to replace the loading-spinner animation 685f0869
* including how to [make it exceptionally normal-looking](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner)
## 🩹 bugfixes
* #136 WebDAV fixes 62ee7f69
* COPY/MOVE/MKCOL: challenge clients to provide the password as necessary
* most clients only need this in PROPFIND, but KDE-Dolphin is more picky
* MOVE: support `webdav://` Destination prefix as used by Dolphin, probably others
* #136 WebDAV: improve support for KDE-Dolphin as client 9d769027
* it masquerades as a graphical browser yet still expects 401, so special-case it with a useragent scan
## 🔧 other changes
* Docker-only: quick hacky fix for the [musl CVE](https://www.openwall.com/lists/musl/2025/02/13/1) until the official fix is out 4d6626b0
* the docker images will be rebuilt when `musl-1.2.5-r9.apk` is released, in 6~24h or so
* until then, there is no support for reading korean XML files when running in docker
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0209-2331 `v1.16.12` RTT
## 🧪 new features
* show rtt (network latency to server, including request processing time) in the top status text d27f1104
* and log the client-reported RTT to serverlog 20ddeb6e
* remember file selection when changing folders c7db08ed
* good for when you accidentally navigate elsewhere
* option to restrict download-as-zip/tar to admins-only c87af9e8
* #135 add [bubbleparty](https://github.com/9001/copyparty/blob/hovudstraum/bin/README.md#bubblepartysh), thx @coderofsalvation! 3582a100
* runs copyparty in a [sandbox](https://github.com/containers/bubblewrap), making it harder to gain unintended access through bugs in python or copyparty
* better alternative to [prisonparty](https://github.com/9001/copyparty/tree/hovudstraum/bin#prisonpartysh), more similar to [the sandboxing in the nixos package](https://github.com/9001/copyparty/blob/7dda77dcb/contrib/nixos/modules/copyparty.nix#L232-L272)
* new plugin: [quickmove](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/quickmove.js) 46f9e9ef
* adds hotkey `W` to quickly move selected files into a subfolder
* #133 new plugin: [graft-thumbs.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/graft-thumbs.js) 6c202eff
* in folders with foobar.mp3 and foobar.png, can copy the thumbnail from the png to the jpg (and then hide the png)
* handlers: add [http-redirect example](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/redirect.py) 22cbd2db
* add [ping.html](https://github.com/9001/copyparty/blob/hovudstraum/srv/ping.html) 7de9d15a 910797cc
## 🩹 bugfixes
* improve iPad detection so they get opus instead of mp3 12dcea4f
## 🔧 other changes
* safeguard against accidental config loss cd71b505
* while no copyparty servers have ended up in this unfortunate situation yet (afaik), be proactive and borrow some experience from other docker-based services
* readme: improve config examples 32e90859
* improve serverlog entries regarding 403s b020fd4a
* #132 mention fuse permissions in readme d9d2a092
* traefik-example: fix disconnect during big uploads 6a9ffe7e
* try to show an appropriate warning for media that the browser doesn't support playing 4ef35263
* was an attempt at detecting iphones failing to play high-color-precision webm files, but safari doesn't seem to realize itself that playback has failed, ah well
* copyparty.exe: update to python 3.12.9
* update deps: dompurify 3.2.4
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0127-0140 `v1.16.11` fix no-acode

2141
docs/chungus.conf Normal file

File diff suppressed because it is too large Load diff

View file

@ -22,6 +22,7 @@
* [dev env setup](#dev-env-setup)
* [just the sfx](#just-the-sfx)
* [build from release tarball](#build-from-release-tarball) - uses the included prebuilt webdeps
* [build from scratch](#build-from-scratch) - how the sausage is made
* [complete release](#complete-release)
* [debugging](#debugging)
* [music playback halting on phones](#music-playback-halting-on-phones) - mostly fine on android
@ -190,6 +191,9 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| GET | `?v` | open image/video/audio in mediaplayer |
| GET | `?txt` | get file at URL as plaintext |
| GET | `?txt=iso-8859-1` | ...with specific charset |
| GET | `?tail` | continuously stream a growing file |
| GET | `?tail=1024` | ...starting from byte 1024 |
| GET | `?tail=-128` | ...starting 128 bytes from the end |
| GET | `?th` | get image/video at URL as thumbnail |
| GET | `?th=opus` | convert audio file to 128kbps opus |
| GET | `?th=caf` | ...in the iOS-proprietary container |
@ -257,6 +261,7 @@ upload modifiers:
|--|--|--|
| GET | `?reload=cfg` | reload config files and rescan volumes |
| GET | `?scan` | initiate a rescan of the volume which provides URL |
| GET | `?scan=/a,/b` | initiate a rescan of volumes `/a` and `/b` |
| GET | `?stack` | show a stacktrace of all threads |
## general
@ -281,8 +286,11 @@ on writing your own [hooks](../README.md#event-hooks)
hooks can cause intentional side-effects, such as redirecting an upload into another location, or creating+indexing additional files, or deleting existing files, by returning json on stdout
* `reloc` can redirect uploads before/after uploading has finished, based on filename, extension, file contents, uploader ip/name etc.
* example: [reloc-by-ext](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reloc-by-ext.py)
* `idx` informs copyparty about a new file to index as a consequence of this upload
* example: [podcast-normalizer.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/podcast-normalizer.py)
* `del` tells copyparty to delete an unrelated file by vpath
* example: ( ´・ω・) nyoro~n
for these to take effect, the hook must be defined with the `c1` flag; see example [reloc-by-ext](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reloc-by-ext.py)
@ -322,7 +330,6 @@ the features you can opt to drop are
* `cm`/easymde, the "fancy" markdown editor, saves ~89k
* `hl`, prism, the syntax hilighter, saves ~41k
* `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
@ -335,7 +342,7 @@ for the `re`pack to work, first run one of the sfx'es once to unpack it
you need python 3.9 or newer due to type hints
the rest is mostly optional; if you need a working env for vscode or similar
setting up a venv with the below packages is only necessary if you want it for vscode or similar
```sh
python3 -m venv .venv
@ -347,7 +354,7 @@ pip install mutagen # audio metadata
pip install pyftpdlib # ftp server
pip install partftpy # tftp server
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install Pillow pillow-heif # thumbnails
pip install pyvips # faster thumbnails
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
@ -389,6 +396,41 @@ python3 setup.py install --skip-build --prefix=/usr --root=$HOME/pe/copyparty
```
## build from scratch
how the sausage is made:
to get started, first `cd` into the `scripts` folder
* the first step is the webdeps; they end up in `../copyparty/web/deps/` for example `../copyparty/web/deps/marked.js.gz` -- if you need to build the webdeps, run `make -C deps-docker`
* this needs rootless podman and the `podman-docker` compat-layer to pretend it's docker, although it *should* be possible to use rootful/rootless docker too
* if you don't have rootless podman/docker then `sudo make -C deps-docker` is fine too
* alternatively, you can entirely skip building the webdeps and instead extract the compiled webdeps from the latest github release with `./make-sfx.sh fast dl-wd`
* next, build `copyparty-sfx.py` by running `./make-sfx.sh gz fast`
* this is a dependency for most of the remaining steps, since they take the sfx as input
* removing `fast` makes it compress better
* removing `gz` too compresses even better, but startup gets slower
* 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 docker-image, you have two options:
* if you want to use podman to build all docker-images for all supported architectures, now run `(cd docker; ./make.sh hclean; ./make.sh hclean pull img)`
* if you want to use docker to build all docker-images for your native architecture, now run `sudo make -C docker`
* if you want to do something else, please take a look at `docker/make.sh` or `docker/Makefile` for inspiration
* if you want to build the windows exe, first grab some snacks and a beer, [you'll need it](https://github.com/9001/copyparty/tree/hovudstraum/scripts/pyinstaller)
the complete list of buildtime dependencies to do a build from scratch is as follows:
* on ubuntu-server, install podman or [docker](https://get.docker.com/), and then `sudo apt install make zip bzip2`
* because ubuntu is specifically what someone asked about :-p
## complete release
also builds the sfx so skip the sfx section above

View file

@ -6,7 +6,7 @@
[global]
e2dsa # enable file indexing and filesystem scanning
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

View file

@ -1,4 +1,3 @@
version: '3'
services:
copyparty:
@ -11,9 +10,17 @@ services:
- ./:/cfg:z
- /path/to/your/fileshare/top/folder:/w:z
environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
healthcheck:
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset"]
# hide it from logs with "/._" so it matches the default --lf-url filter
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset=/._"]
interval: 1m
timeout: 2s
retries: 5

View file

@ -23,6 +23,12 @@ services:
- 'traefik.http.routers.copyparty.tls=true'
- 'traefik.http.routers.copyparty.middlewares=authelia@docker'
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
authelia:
image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax

View file

@ -22,13 +22,13 @@ services:
- 'traefik.http.routers.fs.rule=Host(`fs.example.com`)'
- 'traefik.http.routers.fs.entrypoints=http'
#- 'traefik.http.routers.fs.middlewares=authelia@docker' # TODO: ???
healthcheck:
test: ["CMD-SHELL", "wget --spider -q 127.0.0.1:3923/?reset"]
interval: 1m
timeout: 2s
retries: 5
start_period: 15s
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
environment:
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
PYTHONUNBUFFERED: 1
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
traefik:
image: traefik:v2.11

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