Compare commits

..

794 commits

Author SHA1 Message Date
ed c5c5f9b4b8 readme: add help links 2025-12-04 23:57:54 +00:00
ed fa918228d5 wram: also prevent moves
in addition to write-perms, also drop move-perms from ramdisks
since that is another potential source for confusion

additionally, write-access was correctly prevented, but
the ui would still indicate write permission, so fix that too
2025-12-04 17:50:17 +00:00
ed 29925dc22b update pkgs to 1.19.21 2025-12-02 20:51:10 +00:00
ed cdffde7813 v1.19.21 2025-12-02 20:47:01 +00:00
ed b314e30db8 readme: add server hall of fame 2025-12-02 19:45:08 +00:00
ed d9f76882e7 md-edit: fix sbs in ff52/chrome49 2025-12-02 19:20:04 +00:00
ed 89cab5b520 textfile-viewer: add json-beautifier; closes #794 2025-12-02 17:05:21 +00:00
ed fd8c5bfcbc md-editor: add json beautifier (#794) 2025-12-02 15:54:38 +00:00
ed f7f8716aa8 golf 2025-12-02 15:38:01 +00:00
ed b9aacba130 edland spesial: enda bedre 2025-12-02 14:42:55 +00:00
ed 2e47074627 plugins: warn that rave.js is unmaintained 2025-12-02 14:28:36 +00:00
ed e469bc9445 theme2: reduce grid margins 2025-12-02 14:18:17 +00:00
ed 945b2276a0 fix deadlock on lost windows drive (closes #971);
it would spin on '\\\\?\\g:\\'
2025-12-01 19:23:01 +00:00
ed a9378a8e9c apply gridview volflag on navigation (closes #871);
only takes effect if the user has not specifically toggled
the gridview using either the hotkey or the 田 button
2025-12-01 19:04:50 +00:00
ed a9174e5dee ui-option to force-download files (closes #1058);
* button "dl" in settings UI (always takes precedence)
* global-option and/or volflag "dlni"
* url-parameter ?dlni or ?dlni=0

the preference is applied per-volume when navigating between folders,
unless the settings-button has been toggled, which overrides that
2025-12-01 19:02:03 +00:00
ed fcc1bdfbf5 decode ansi-colors in .txt/nfo files; closes #1064 2025-12-01 17:38:19 +00:00
ed 278a0d8548 md: rewrite links to open in viewer; closes #972 2025-12-01 17:23:47 +00:00
ed 04ac7fbd21 shares: remove delete-permission (closes #1023);
until now, shares could be created with permissions read/write/delete
(any combination thereof), however the delete option was never fully
implemented and dysfunctional, hence now removed

using vn0/rem0 throughout _handle_rm would almost be sufficient
however the primary concern is ensuring integrity of metadata tables,
and _forget_file expects a dbv rather than the share's vn
2025-12-01 16:32:33 +00:00
ed ba7387209a github: update issue templates 2025-12-01 14:40:18 +00:00
ed a31bfe6b2b
update security policy
Signed-off-by: ed <s@ocv.me>
2025-12-01 06:26:08 +01:00
ed dba7c5d4d5 iOS: bbox: fix video scrubbing;
unlike android, iOS does not eat touch-events in the video controls,
so it would switch to the prev/next media on seek instead of seek
2025-11-30 22:14:26 +00:00
ed acde21d484 fix controlpanel greeting in early responses;
responses sent early during request processing (primarily for
invalid requests) would display the username " " rater than "*"
in the controlpanel, in one case leading to user confusion
2025-11-30 20:36:32 +00:00
ed cedfc44420 panic if --shr overlaps with volumes 2025-11-30 20:06:56 +00:00
ed f4d67ff031 fix double pathsep in ongoing-xfer links 2025-11-30 19:59:57 +00:00
ed 889bd3242a hooks: retcode 100, zmq json;
hooks returning exitcode 0 will:
* run the next hook, if any
* allow the original action, unless successive hook opposes

hooks returning exitcode 100 will:
* abort running successive hooks
* allow the original action

hooks returning anything other than 0 or 100 will:
* abort running successive hooks
* REJECT the original action

zmq can now respond with json; a dict with "rc", "rejectmsg",
"reloc" and so on, just like other hooks replying with json
2025-11-30 19:29:09 +00:00
shermanhlc ca6d3a5c16
fix audio-volume scrollwheel imprecision (#1054) 2025-11-30 17:24:28 +00:00
ed 98701b78e6 fix worker init timeout (closes #1035);
context: if webworkers fail to initialize within 5sec,
up2k falls back to hashing on the main-thread instead

problem: if webworkers eventually do finish init,
they would then be racing the mainthread

fix: disconnect webworkers if init timeout

additionally, gradually extend the timeout as long as
the workers are still making progress initializing
2025-11-30 14:57:21 +00:00
ed a28503e805 audioplayer: fix mode=stop panic; closes #1022
js would panic if switching to prev song at start of folder
2025-11-30 10:51:10 +00:00
ed ec00dc18ca add cfssl debug (#1030) 2025-11-30 10:35:21 +00:00
ed 7f9c139ead helptext anchors 2025-11-30 10:34:50 +01:00
ed 3f4b79ffb8 xm: ignore upload-queue-empty
the message is sent by up2k when all uploads done; #949
2025-11-29 07:54:52 +01:00
Jonas 867237d06a
docker/idp: fix config typo (#1032)
Removed write permissions from root volume.

Signed-off-by: Jonas <55783419+tuetenk0pp@users.noreply.github.com>
2025-11-28 20:40:36 +01:00
shelvacu c7345308f5
nix-overlay: option to install from git-head (#868)
Co-authored-by: Shelvacu <git@shelvacu.com>
2025-11-26 12:15:39 +01:00
ed fb9f0441c9 fix possible deadlock on shutdown;
if a ?tar=w is hammering the thumbnailer queue on shutdown, give up
issuing sentinels (the workers will see !!stopping and abort anyways)
2025-11-23 21:45:03 +09:00
ed a359b89edd support thumbnail pregen for all output formats 2025-11-23 21:43:55 +09:00
Habetdin 77f74ddb2f
fix ongoing-xfer links in controlpanel (#977)
when viewing the controlpanel from a path other than the webroot,
the links to ongoing transfers were invalid
2025-11-19 16:52:31 +01:00
ed f7e7b03f6f reduce redirect delay 2025-11-20 00:48:34 +09:00
ed 7a291403ca contextual login caption; closes #1012 2025-11-20 00:46:19 +09:00
ed b427d7802a metrics: allow list of users 2025-11-20 00:37:36 +09:00
ed c424a55d6f more hints for invalid config 2025-11-20 00:26:44 +09:00
ed 7d62335c13 repurpose new-md to new-textfile 2025-11-20 00:24:28 +09:00
ed 9385daeae8 fix pypi packaging; closes #1003 2025-11-19 23:49:23 +09:00
ed 79e1078671 panic if unknown users in ipu; closes #959 2025-11-19 23:28:39 +09:00
ed cad15fbf60 warn against changing -j 2025-11-19 23:26:19 +09:00
Robert Ismo 9f08efcabd
inverse unix philosophy (#994)
Signed-off-by: Robert Ismo <robertismo@protonmail.com>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-11-19 15:24:52 +01:00
ed ac085b8149 mtag: fix geotag usage 2025-11-02 14:56:28 +00:00
ed 1c15c0d5d1 mtag: add geotag.py 2025-11-02 13:12:13 +00:00
Nicolas Mémeint 904c984bda
nixos: Only create and bind volumes without variables (#962) 2025-11-02 12:59:48 +00:00
Nicolas Mémeint 3242145e52
nixos: Use volume/global chmod-d for module directory creation (#963) 2025-11-02 12:59:41 +00:00
ed dff6aa2435 update pkgs to 1.19.20 2025-11-02 01:13:32 +00:00
ed 450cd86dc1 v1.19.20 2025-11-02 01:09:40 +00:00
ed db60951d9f apply per-vol (md|lg)_sb during nav 2025-11-02 00:58:13 +00:00
Carson c00314a292
apply per-volume sb_lg during navigation (#967)
Signed-off-by: Carson <57198646+carson-coder@users.noreply.github.com>
2025-11-02 00:29:12 +00:00
ed 2cc53ea151 list files in /?shares; closes #961 2025-10-31 23:09:14 +00:00
ed e9ab040ce8 docker: fix crossbuild from aarch64 2025-10-26 23:30:57 +01:00
ed e005930cd0 readme: pyvips on windows 2025-10-26 23:23:17 +01:00
ed 4fcd2c4193 update pkgs to 1.19.19 2025-10-25 19:21:56 +00:00
ed cec44aa1dd v1.19.19 2025-10-25 19:18:51 +00:00
ed e3524d85bd fix for archlinux pkgbuild 2025-10-25 19:02:06 +00:00
ed 1963ed1795 update pkgs to 1.19.18 2025-10-25 14:36:57 +00:00
ed f0297777eb v1.19.18 2025-10-25 14:34:01 +00:00
ed 36ab77e0bf fix og-specific issue with single-file volumes;
if a textfile was shared with permission r below/inside a more
restrictive volume, then:

* if the parent volume had either permission g or h, then the files in
  the parent folder could be accessed

* if the parent volume had anything stricter than g or h, then filenames
  in the parent volume could be seen, but not accessed
2025-10-25 14:23:55 +00:00
ed db87ea5ce1 send msg when uploads finished; closes #949 2025-10-25 14:00:29 +00:00
ed b1efc0065c disable e2d for single-file volumes;
avoids some warnings in the log
2025-10-25 13:06:04 +00:00
ed a325353b1b option to default-disable tooltips; closes #937 2025-10-25 00:39:37 +00:00
ed 1c17b63b76 fix filesize color on select 2025-10-25 00:15:36 +00:00
ed d3dd34569a u2c: fix delete-url (closes #948);
u2c would produce an URL which triggered a windows-only
sanchk serverside, rejecting the delete-request
2025-10-24 21:48:59 +00:00
ed cdd5e78adf fix unlistc* when parent is jumpvol 2025-10-23 21:54:32 +00:00
ed fff7291dcf show h vols in ls and tree,
and compensate with some optimizations
2025-10-23 21:44:28 +00:00
ed 7f5810f1a7 ENTERPRISE packaging (closes #941);
introduce copyparty-en.pyz, affectionately known as companyparty.pyz

like copyparty-en.py, it is english-only but with a twist;
also remove smb support because the impacket references can look sus
2025-10-22 22:20:57 +00:00
ed b624a38747 ENTERPRISE file extensions (#941 omake);
in case a snakeoil salesman managed to convince your workplace into
purchasing their "internet security solution" which blocks downloads
of certain files according to file extensions -- or, in other words,
smoke and mirrors, with a comforting false sense of security
2025-10-22 22:10:02 +00:00
ed c9e45c12d8 fix permission h with og 2025-10-22 21:00:34 +00:00
/dev/urandom 5c42ad1c78
tl/esperanto: fix capitalization (#936)
Signed-off-by: /dev/urandom <53902042+slashdevslashurandom@users.noreply.github.com>
2025-10-22 19:44:05 +00:00
ed 547a7ab1cc add download mirror https://copyparty.eu/ 2025-10-19 18:19:45 +00:00
ed a04570ff5e packaging: fix $PWD assumption 2025-10-19 17:53:43 +00:00
ed 93eb862c60 docker: replace confusing config example 2025-10-19 17:52:03 +00:00
ed 81881a449b update pkgs to 1.19.17 2025-10-17 23:20:30 +00:00
ed e2a15a3a92 v1.19.17 2025-10-17 23:13:50 +00:00
ed 51e6099c58 add --versionb; closes #933 2025-10-17 22:30:14 +00:00
ed f73742b6a4 release-automation 2025-10-17 21:59:37 +00:00
ed 5d96862c45 lifetime maxval; closes #926 2025-10-17 18:59:13 +00:00
ed 98da5cc509 ui-elements can be hidden (#928) 2025-10-17 16:29:55 +00:00
ed d9cd7ec3cf race-the-beam in shares 2025-10-17 00:19:58 +00:00
ed dbb78705f7 fix textviewer with g permission; closes #927
textfiles can also be viewed with `foo.txt?v` for less jank
2025-10-17 00:12:21 +00:00
ed b4fda5f1d6 subtract users/groups from access; closes #845 2025-10-17 00:01:54 +00:00
AppleTheGolden 1eb016c975
fix --accounts typo in --help-auth (#931) 2025-10-17 01:14:20 +02:00
ed f55d8341f1 new flag nodupem: reject dupes on move; closes #742 2025-10-14 22:51:48 +00:00
ed 9746b4e21e mediasession: embedded coverart as fallback 2025-10-14 22:02:09 +00:00
ed ad85713693 indent 2025-10-14 22:01:21 +00:00
ed 66dc8b5c9f bbox: zoom to fit; closes #905 2025-10-14 21:45:41 +00:00
ed 740d22d3ba nice 2025-10-14 21:17:34 +00:00
ed 6f02812af1 disable bans according to perms; closes #911 2025-10-14 21:09:38 +00:00
ed 3364448858 xm-hook: include full selection path (#921) 2025-10-14 20:30:12 +00:00
ed 677fd8eef1 config for ?stack verbosity / access 2025-10-14 20:23:38 +00:00
ed cca1f9b22b hide search-ui in shares 2025-10-14 20:12:26 +00:00
ed 6c53c75ab2 black 2025-10-14 20:10:47 +00:00
ed 6c024dbf80 xm-hooks: list of selected files; closes #921
previously, `xm` hooks would be called with the `txt` property
containing the url-decoded message

now, a new property `body` contains the original unmodified message,
to avoid any ambiguity caused by url-decoding

if any files are selected, the list of files is appended to
the `txt` field as lines, and as `sel` url-parameters in `body`

Co-authored-by: Carson Coder <carson@carsoncoder.com>
2025-10-14 19:39:03 +00:00
ed d099e5e84e tl-split: draw the rest of the owl 2025-10-12 22:49:47 +00:00
ed ff01723c6d tl-split: tl.js 2025-10-12 22:28:45 +00:00
ed 02ba9ea7ec tl-split: rm orig from splash.js 2025-10-12 20:33:25 +00:00
ed 5232ce6a53 tl-split: rm orig from browser.js 2025-10-12 20:30:14 +00:00
ed f5425a8839 tl-split: mtl strings 2025-10-12 20:27:02 +00:00
NandeMD 3b009d97b3 tl-split: Turkish
original commit: 2025-09-22, 549fe33
2025-10-12 20:24:03 +00:00
/dev/urandom 50109f7631 tl-split: Esperanto
original commit: 2025-09-21, 15d3c2f
2025-10-12 20:23:59 +00:00
4ntsy 14bd4cf5c1 tl-split: Portuguese
original commit: 2025-08-23, 4b8c221
2025-10-12 20:23:42 +00:00
nyqui 28b9323890 tl-split: Korean
original commit: 2025-08-14, 4e878d2
2025-10-12 20:23:38 +00:00
Bevinsky b61b910e2a tl-split: Swedish
original commit: 2025-08-14, d676a86
2025-10-12 20:23:33 +00:00
Tr3yWay996 07ace41640 tl-split: French
original commit: 2025-08-09, e9ddfcc
original commit: 2025-08-10, 7aa2148

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-10-12 20:23:28 +00:00
Artur Borecki 748aaa950c tl-split: Polish
original commit: 2025-08-08, 392a4db
original commit: 2025-09-08, 52438bc

Co-authored-by: dai <contact@daimond113.com>
2025-10-12 20:23:24 +00:00
Kent Daleng 6de6aa4bb8 tl-split: Norwegian-Nynorsk
original commit: 2025-08-08, 3931bc2
2025-10-12 20:23:19 +00:00
Jakub Pelc 0fa862e145 tl-split: Czech
original commit: 2025-08-07, c955658
2025-10-12 20:23:05 +00:00
chamdim bb166c98c9 tl-split: Greek
original commit: 2025-08-06, 50f4618
original commit: 2025-08-08, bd514f0
2025-10-12 20:22:58 +00:00
AOTREVAI 52446bb507 tl-split: Italian
original commit: 2025-08-03, a38e6e6
2025-10-12 20:22:54 +00:00
DeStilleGast 5a29df6ba9 tl-split: Dutch
original commit: 2025-08-03, 3798e19
2025-10-12 20:22:50 +00:00
exci b9d7ede3d6 tl-split: Finnish
original commit: 2025-08-01, 7ecedb2
original commit: 2025-08-03, a68d5b0

Co-authored-by: icxes <icxes@dev.null@need.moe>
2025-10-12 20:22:46 +00:00
Juan Herruzo c2e03bf6bc tl-split: Spanish
original commit: 2025-08-01, 4e68379
original commit: 2025-08-01, 89690b9
2025-10-12 20:22:42 +00:00
Altair a85ad20142 tl-split: Russian
original commit: 2025-07-31, 0b05c72
2025-10-12 20:22:38 +00:00
Vlad 98086948df tl-split: Ukrainian
original commit: 2025-07-31, fea45e4
original commit: 2025-08-08, 0dd5987
2025-10-12 20:22:34 +00:00
Raphael Guntersweiler a7cdc5de08 tl-split: German
original commit: 2025-07-30, 9d32564
2025-10-12 20:22:14 +00:00
ultwcz a7840bebe7 tl-split: Chinese
original commit: 2024-08-22, 92edea1
original commit: 2024-08-23, 55a77c5
original commit: 2024-09-11, 7a573ca
2025-10-12 20:21:50 +00:00
ed d6433b784b tl-split: Norwegian-Bokmål 2025-10-12 20:21:43 +00:00
Audionut 33b0cd5a34
fix ftp append (#914)
previously, the target file would always be unlinked upon upload resume,
contrary to what the client expects (open for append).

when a client sends an APPE, pyftpdlib will `ftp_STOR(file, "a")` which
is something that should be allowed within the ftp-wt grace period
2025-10-12 00:32:51 +00:00
ed f2caab6119 dedup: explicit reflink/ficlone on python<3.14 2025-10-11 23:43:09 +00:00
ed e65ec75e22 fix tests 2025-10-11 23:42:34 +00:00
AppleTheGolden 7be67ab474 format bos.py with black 2025-10-11 23:35:57 +00:00
AppleTheGolden 8ef6dda74b
view .cbz in browser (#916)
adds functionality to allow browsing .cbz directly in the browser, without downloading them and using a separate program. meant for quickly inspecting the contents, less so for reading.

adds two new api calls, ?zls and ?zget, which return a file listing of a zip file and a specific file in the archive, respectively.

uses the zipfile module, so no support for .cbr etc
2025-10-11 23:17:24 +00:00
ed 46c205dd60 sfx-packer optimizations 2025-10-11 21:22:22 +00:00
ed dbd8f837e8 hooks: add wget-i.py (import-safe) (#904) 2025-10-08 22:55:19 +00:00
ed 20ac117c32 update pkgs to 1.19.16 2025-10-05 23:08:25 +00:00
ed cd3feaac86 v1.19.16 2025-10-05 22:59:38 +00:00
ed f8e19815e1 mention ?v suffix for media links (#895) 2025-10-05 21:58:27 +00:00
ed 76e9f23a6d batch-rename: initial counter values 2025-10-05 21:51:41 +00:00
ed 4542ad3c01 hook-flag to send input on stdin 2025-10-05 20:35:03 +00:00
ed 50276c0cfa show mediatags in shares 2025-10-05 20:06:17 +00:00
ed c5f1229685 hide new-share btn in shares 2025-10-05 18:36:10 +00:00
ed 73ec2d296f rss: option to not embed pw in feed 2025-10-05 18:09:05 +00:00
ed a499648291 recommend libvips for heic/heif 2025-10-05 17:24:55 +00:00
ed 4bb5baf1b8 dangit 2025-10-05 00:44:07 +00:00
ed efd19af7ca new hook: granular ramdisk detection 2025-10-05 00:13:34 +00:00
ed aace711eb9 unvendorable surrogateescape (#887);
stolen/surrogateescape.py can be deleted;
this file is only necessary for python2 compatibility
2025-10-04 22:10:34 +00:00
ed 39bd4e5b58 unvendorable dnslib (#887);
stolen/dnslib/ can be deleted and replaced with system lib

NOTE: unvendoring dnslib will make it impossible to communicate with
  devices which have a particular avahi bug; see 6c1cf68bca
2025-10-04 22:09:40 +00:00
ed 08ebb0b4c9 unvendorable qrcodegen (#887);
move copyparty-original code to qrkode.py

stolen/qrcodegen.py can be deleted and replaced with system lib

this is safe and has minimal affect on functionality;
performance will be a tiny bit slower without the vendored copy
2025-10-04 21:36:41 +00:00
ed 656f0a6c39 unvendorable ifaddr (#887);
stolen/ifaddr/ can be deleted and replaced with system lib;
this is safe and will not affect any functionality
2025-10-04 21:33:01 +00:00
ed 805a7054e9 add missing licenses (#887);
* added missing license entry for fusepy
* added missing license entry for DOMPurify
* aligned license names with SPDX identifiers
2025-10-04 21:29:08 +00:00
ed 01709691f2 git-mv ofl.r13 2025-10-04 21:22:28 +00:00
ed 41ed559faa hooks: import-flag 2025-10-04 13:32:26 +00:00
ed a0f8f794a8 mention termux fix 2025-10-04 12:03:37 +00:00
ed fbe5fa582e helptext fix 2025-10-04 12:00:42 +00:00
ed 2248705e1a --ui-filesz can have trailing dash 2025-10-04 11:59:53 +00:00
ed eb173be4f1 folder-thumbs: cbz, epub 2025-10-04 11:57:43 +00:00
ed d05a88d2ee add rename counters; closes #854 2025-10-04 11:53:59 +00:00
ed 09e6f29e5e indent 2025-10-04 11:42:43 +00:00
ed 2ce32e4fb6 apply vol-favicon on nav; closes #882 2025-10-04 11:16:16 +00:00
ed 9b7f933b78 optimize --name-url (#884) 2025-10-04 10:45:17 +00:00
Lulu 38cc809822
Add --name-url option (#884)
Turns the server name into a hyperlink to a spefified URL

Can link back to homepage with `--name-url=/`, controlpanel with
`name-url="/?h"`, or external sites with `name-url="https://foo.bar/"`
2025-10-04 10:10:48 +00:00
ed e9b6e645d3 fix buildscript perm 2025-10-04 09:41:05 +00:00
ed 0f9a239078 allow favicon.png/gif (samsung-android) 2025-10-04 09:39:13 +00:00
ed 0453b7ac53 xhrchk: generic error only as fallback 2025-10-04 09:38:34 +00:00
Chloe Surett 1bcdf8c9e3
Add Blu-ray discs to fuzzy file size type (#878)
Signed-off-by: Chloe Surett <chloe@surett.me>
Signed-off-by: ed <s@ocv.me>
Co-authored-by: ed <s@ocv.me>
2025-10-04 09:35:58 +00:00
ed 4177c1d9ed epub: handle missing covers; closes #860 2025-10-04 09:30:43 +00:00
ed 171ca985c8 bbox: flex conditionally 2025-10-04 09:23:42 +00:00
AppleTheGolden dacc64dd2e
baguettebox: RTL support (#881) 2025-10-04 09:12:02 +00:00
Kaleb Debre 31f1b535b2
nixos: unix-user/group to run as (#886)
Co-authored-by: Kaleb Debre <kaleb.debre@web.de>
2025-10-03 17:13:28 +02:00
Daniel Lovegrove 7fc379abc8
Add setup example for running with Podman under systemd (#460)
* Create copyparty.container file
* Add and document rootful configuration
* Add non-root config, clean up README

Signed-off-by: Daniel Lovegrove <d.lovegrove11@gmail.com>
2025-10-03 05:45:44 +02:00
ed a8f53d5ef0 shrink docker-min from 45 to 33 MiB 2025-09-30 23:13:46 +00:00
ed 3f59710294 allow chpw with idp; closes #872 2025-09-30 21:26:37 +00:00
ed 24e01221c5 update pkgs to 1.19.15 2025-09-29 23:19:00 +00:00
ed daba1ab7bd v1.19.15 2025-09-29 23:10:18 +00:00
ed 1bca86c6e1 thx pyright 2025-09-29 23:07:03 +00:00
ed fc2754cba5 option to delete .PARTIAL on expiration 2025-09-29 22:16:57 +00:00
ed 470b504843 raster favicons; closes #383, #473 2025-09-29 21:50:13 +00:00
ed 435db14798 pregen html_head when static 2025-09-29 21:47:53 +00:00
ed d08e872062 formatting 2025-09-28 22:23:02 +00:00
ed f91a653bde zed pls 2025-09-28 22:22:27 +00:00
ed 7d86f39a23 uds-only mdns fix; closes #864 2025-09-28 22:17:30 +00:00
ed 456addf26f show warks in folder-listings 2025-09-28 21:37:46 +00:00
ed 4e38e4087e make warks (checksums) searchable 2025-09-28 21:36:45 +00:00
suza f0ecb08347
fix(opengraph): video embeds (#870) 2025-09-28 20:13:37 +00:00
ed 1193f9ba6c stop binary garbage from hitting logs 2025-09-28 19:29:18 +00:00
ed 234eddec90 filesize formats; closes #184 2025-09-28 01:14:00 +00:00
ed e3baf932f3 reflinks are non-e2d safe 2025-09-27 19:29:36 +00:00
ed eb5d767b01 MTHash: fully preserve exception info 2025-09-27 19:28:41 +00:00
ed ec7418734d uds-only http/https; closes #855 2025-09-27 19:12:06 +00:00
ed a3d9506783 mdns: customize http/https ports (#855) 2025-09-27 19:11:15 +00:00
ed 57650a218f use reflinks (not hardlinks) in -ss; closes #858 2025-09-27 18:44:14 +00:00
ed 983865d96c cbz thumbs without ffmpeg; closes #859 2025-09-27 18:39:18 +00:00
ed 6f6b70ad04 fix misleading comments 2025-09-27 00:22:28 +00:00
ed e187df28f2 fix markdown-expand example 2025-09-26 23:50:19 +00:00
ed df0fa9d1b7 xbu/xau with custom message 2025-09-26 23:49:32 +00:00
ed 397ed5653b overwrite on upload with header "replace" 2025-09-26 21:35:00 +00:00
ed 9f46e4dbd7 koie layout 2025-09-26 21:17:07 +00:00
ed 6912e86747 loud warning on EIO (HDD/FS trouble) (#851) 2025-09-25 23:18:09 +00:00
ed 80ca78516e create idp-db when necessary (#849) 2025-09-24 21:32:15 +00:00
ed a493cd6530 update pkgs to 1.19.14 2025-09-23 23:06:19 +00:00
ed c72b62ad86 v1.19.14; closes #847 2025-09-23 22:47:07 +00:00
ed fdcd92bac8 update pkgs to 1.19.13 2025-09-23 21:43:35 +00:00
ed b00dac997a v1.19.13 2025-09-23 21:20:13 +00:00
ed 9d066414c6 friendly http-5XX errors 2025-09-23 20:42:06 +00:00
ed 5e4ff90b1c config-loader: support utf8-bom 2025-09-23 20:00:45 +00:00
ed deb8a4a86e readme: ./scripts/toc.sh 2025-09-23 19:40:40 +00:00
ed 733e85c040 cosmetic: fix uptoast on page load 2025-09-23 19:38:24 +00:00
ed 892a452446 tl cleanup 2025-09-23 19:35:34 +00:00
ed 38df223b8f reindent: try/catch subchunk logic;
ensures chunks get unlocked on throw

reindent of fc8298c468
2025-09-23 19:35:21 +00:00
ed b136a5b042 fast_confirm_chunks: release all on error;
possibly fixes an issue someone has been runnning into:

an upload could get stuck on "that chunk is already being written to"
when the server was overloaded enough that connections kept dropping
2025-09-23 19:11:41 +00:00
ed 377eddcd06 tl cleanup 2025-09-22 21:15:26 +00:00
NandeMD 549fe33f51
add Turkish translation (#786)
Signed-off-by: NandeMD <76772692+NandeMD@users.noreply.github.com>
2025-09-22 21:13:18 +00:00
ed c214a93caa opdf: fix merge issues 2025-09-22 20:37:33 +00:00
ed 0941fd4ec1 ensure ?ls not from tx_ls_vols (unmapped root)
fixes regression in 8f6194fe
2025-09-22 19:58:41 +00:00
AppleTheGolden 6dbd9901b2
OPDS Support (#779)
* add OPDS support
* add `?opds` to devnotes.md
* send content-disposition for opds downloads
2025-09-22 19:34:34 +00:00
Taylor e9ca36fa88 versus.md: add links with tooltips to the comparison matrices 2025-09-22 18:12:16 +00:00
augustanational a053a663b4
versus.md: sftpgo (#839)
Signed-off-by: augustanational <milanistavietnam1911@gmail.com>
2025-09-22 16:24:14 +02:00
ed 1923a25879 update pkgs to 1.19.12 2025-09-21 22:21:44 +00:00
ed 4cce799012 v1.19.12 2025-09-21 22:11:56 +00:00
ed 1460fe97ac rotf timezone option; closes #802 2025-09-21 21:21:41 +00:00
ed ca872c4055 hide unpost tab in read-only vols; closes #836 2025-09-21 20:45:56 +00:00
ed 3ddb4c042a tl cleanup 2025-09-21 19:59:29 +00:00
/dev/urandom 15d3c2fbff
Esperanto translation (#787) 2025-09-21 19:53:33 +00:00
ed 6a24432019 windows: fix sharing entire drives (closes #837);
broke in 8b66874b
2025-09-21 19:44:35 +00:00
ed 8f6194fe77 /?ls on unmapped root 2025-09-21 19:35:28 +00:00
25huizengek1 260da2f45c fix: rootless podman/docker not building correctly on NixOS 2025-09-21 00:36:32 +00:00
ed a9e02ce753 update pkgs to 1.19.11 2025-09-20 10:17:59 +00:00
ed 70c088aeca v1.19.11 2025-09-20 10:11:03 +00:00
ed 280815f158 ftp: fix unmapped root; closes #827 2025-09-20 10:05:29 +00:00
ed e1ea9852c6 update pkgs to 1.19.10 2025-09-19 22:52:29 +00:00
ed 2ee9c80d3b v1.19.10 2025-09-19 22:44:54 +00:00
ed 4b2ff3a196 epub-thumbs errorhandling 2025-09-19 22:19:01 +00:00
ed 538a205ce4 fix up2k fstab after 59a01221 2025-09-19 21:50:21 +00:00
ed 6559152882 add free-threading suffix to version dump 2025-09-19 21:27:13 +00:00
ed 669b10754d copyparty32.exe: english-only 2025-09-19 21:21:44 +00:00
ed 478f1c764e logging: limit/disable fs-indexing progress 2025-09-19 21:05:42 +00:00
ed a043d7cfb6 explain daw better 2025-09-19 20:35:01 +00:00
Nora Struck ee5f31908f Add groups to nix config 2025-09-19 20:15:30 +00:00
ed 35326a6fb8 iOS: fix a-shell docs; closes #806 2025-09-19 20:13:10 +00:00
ed 59a0122179 prevent upload into ramdisk;
tries to detect misconfigured docker environments, e.g. /w/foo is mapped
to a disk but /w/ itself isn't
2025-09-19 19:46:14 +00:00
ed 5996a58b20 fix medialinks sans ls0 (closes #809);
on a browser's very first visit, the first page load would not hydrate
correctly, initializing msel without file-IDs, causing medialinks
(#gf-0f6f5c0d) to throw an error
2025-09-16 22:28:41 +00:00
ed fd331a545d nice typo 2025-09-15 01:05:17 +00:00
ed e7ef31ee33 update pkgs to 1.19.9 2025-09-15 00:44:51 +00:00
ed 49ce67e9cd v1.19.9 2025-09-15 00:19:24 +00:00
ed 8b66874b85 be case-sensitive on windows/macos (closes #781);
on Windows and Macos, most filesystems are case-insensitive,
which can lead to dangerous situations

one example is when another program (not copyparty or its UI) wants to
rename a file from `Foo` to `foo`; the program will probably start by
checking if `foo` exists and then delete it, however this would match
`Foo` and confuse the program into deleting the wrong file

fix this by adding a VERY EXPENSIVE detector to prevent this,
by listing the parent folder and checking if the case matches

this check will auto-enable when a case-insensitive FS is detected on
startup, but option `casechk` (global or volflag) can override this
2025-09-14 23:39:46 +00:00
ed 3a2381ff2d webdav: fix depth:0 in rootless vfs;
also safeguards against potential issues with invalid
paths if the api is used incorrectly from a plugin
2025-09-14 23:27:33 +00:00
ed 83bd197438 warn on invalid idp-volume mapping 2025-09-14 23:20:45 +00:00
ed c03b332ec0 linter fixes 2025-09-14 23:18:34 +00:00
ed 17b4f905a7 misc buildscript / docs:
* make-sfx.sh: fix missing licenses on very first build
* docker/make.sh: add warning on missing deps
* pyinstaller: cleanup notes
* add notes:
  * building on archlinux
  * buliding python 2.7
* support for Zed IDE
2025-09-14 23:15:18 +00:00
ed 8f587627e1 download-as-zip: better names for selections;
previously, the first selected file became the name of the zip

now, the name will be "sel-foldername", similar to when the whole
folder is downloaded, but with a prefix to indicate it's a subset
2025-09-13 21:10:10 +00:00
ed 14b7e5143f md-editor: fix extra http roundtrip;
the check for a leading newline was not specific enough,
accidentally matching the opening line of a json document,
triggering the xhr safeguard and wasting a roundtrip
2025-09-13 21:09:59 +00:00
ed 3e97a77784 cosmetic fixes after edafa1586 2025-09-13 21:04:16 +00:00
ed 3f45492725 fix --help on non-utf8 consoles 2025-09-13 20:59:00 +00:00
ed 9c9e4057e0 media-tags: unmap "conductor" (was album-artist) 2025-09-13 20:55:08 +00:00
ed 3d09bec1bb fix ?v to opt-out from index.html 2025-09-13 20:53:12 +00:00
ed d7887f3d55 qr-code can optionally ignore -q;
new options --qr-stdout and --qr-stderr will always print the qr-code
into the console, even if copyparty is running in quiet mode (-q)

closes https://codeberg.org/9001/copyparty/issues/1
2025-09-13 20:14:50 +00:00
ed ecd18adc3c optional case-insensitive unicode search (#789);
made it default-disabled because it's a bit expensive
2025-09-13 19:48:08 +00:00
Kyler Clay e2aa8fc1a4
case-insensitive search for non-ascii filenames/paths (#789)
the previous case-insensitive searching of filenames and paths
would only apply to ascii letters; extend this to all characters
2025-09-13 19:44:26 +00:00
Beethoven dfd9e007ee
debian: Fix launcher path in systemd service (#798)
assume launcher is in /usr/local/bin/ rather than /usr/bin
which is the case as of recently

Signed-off-by: Beethoven <44652883+Beethoven-n@users.noreply.github.com>
2025-09-13 18:45:53 +00:00
David Sullivan 5c1a43c711
fix hotkey typo (P->O) in lightbox (#788)
helptext indicated that P was the hotkey for seeking

Signed-off-by: David Sullivan <311316+tkroo@users.noreply.github.com>
2025-09-11 00:31:58 +02:00
daimond113 52438bcc0d update polish pluralization 2025-09-08 23:18:18 +02:00
ed e09f3c9e2c shutil: ignore errors from copystat in copy2;
ntfs on linux can be picky about cloning mtime onto a new file;
generally we don't care if that fails, however, we also want the
speedup that CopyFile2 can offer, so cannot use copyfile directly

this avoids the following issue:

up2k:3537 <_symlink>: shutil.copy2(fsenc(csrc), fsenc(dst))
shutil:437 <copy2>: copystat(src, dst, follow_symlinks=follow_sym[...]
shutil:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, s[...]
[PermissionError] [Errno 1] Operation not permitted, '/windows/videos'
2025-09-08 20:21:12 +00:00
ed 25749b4b5f accept empty files through bup; closes #775 2025-09-08 10:08:34 +02:00
ed 75b0b312a4 update pkgs to 1.19.8 2025-09-07 23:16:36 +00:00
ed c47c708433 v1.19.8 2025-09-07 23:00:05 +00:00
ed e0a92ba72d fence fileshares to just those files
when a share is created for just a single file, it was possible to
guess other filenames in the source folder and access those files
2025-09-07 22:48:31 +00:00
ed 98386f28f0 simplify og_ua logic;
idk what this was *supposed* to do but what it *did* was prevent
loading the full image even when the request had a good referrer
(this broke viewing images in firefox at least)
2025-09-07 20:54:37 +00:00
ed 422f8f624e fix volflag og_ua 2025-09-07 20:42:23 +00:00
ed edafa1586a volflag to block sharing of a volume 2025-09-07 17:20:51 +00:00
ed e270fe60ed fix uds perms with rm-sck 2025-09-07 09:02:43 +00:00
ed ab56238249 docker: fix image annotations;
docker buildx imagetools inspect copyparty/ac:beta@sha256:[...] --raw
would show the annotations from the base alpine image instead of ours

thx to @EmilyxFox for figuring this out!
2025-09-06 23:44:48 +00:00
ed 3bdef75e88 connectpage: usernames 2025-09-06 22:17:48 +00:00
ed 67ba5b0252 partyfuse: suggest fuse2 2025-09-06 22:12:07 +00:00
ed 06d2654b3f partyfuse: readdir from cache;
dircache only applied to `getattr` and not `readdir` itself
2025-09-06 21:31:09 +00:00
ed 1cdb388090 partyfuse: usernames 2025-09-06 21:00:21 +00:00
ed f7e08ed007 defer next-song hotkey while changing folders 2025-09-05 23:19:20 +00:00
ed b049631169 ftp: CWD is optional (#539) 2025-09-05 22:36:16 +00:00
ed aaeec11f81 bail from aborted batch operations; closes #748
f.shift() in rename_cb would return null since the queue was dumped
2025-09-05 21:43:33 +00:00
ed 96b109b0d6 decrement folder-sz on delete; closes #759, #393 2025-09-05 21:03:30 +00:00
ed 74821a38ad speed 2025-09-05 20:38:04 +00:00
ed 19a4c45389 rbac disk-info and --ver (closes #726);
options --du-who and --ver-who specifies who can see the disk-info
(disk-usage, disk-free) and server-version based on user permissions
2025-09-05 19:48:38 +00:00
ed 09f22993be idp login/logout routes (#761) 2025-09-05 18:44:30 +00:00
ed c2be664e96 cleanup jinja whitespace 2025-09-05 18:12:53 +00:00
ed 7a4ee4dbc8 apply ipr during login too (#397) 2025-09-05 16:20:00 +00:00
ed bd6d1f961d konmai intensifies
thx SG
2025-09-04 23:48:22 +00:00
ed eeb7738b53 clamp utime to filesystem limits (#539) 2025-09-04 23:31:05 +00:00
ed e6755aa8a1 restrict runtime-state in $TMP; closes #747
the preferred locations (XDG_CONFIG_HOME and ~/.config)
are trusted and will behave as before, because they are
only writable by the current unix-user

but when an emergency fallback location ($TMPDIR or /tmp) is used
because none of the preferred locations are writable, then this
will now force-disable sessions-db, idp-db, chpw, and shares

this security safeguard can be overridden with --unsafe-state

will now also create the config folder with chmod 700 (rwx------)
2025-09-03 21:55:07 +00:00
ed 230a146209 ignore dotfiles in config-folders; closes #727
macos adds garbage files named ._something.conf
into config folders, crashing the config parser
2025-09-03 19:57:28 +00:00
ed c71128fd72 ignore cppws on plaintext;
cppws, if set from https context, cannot be cleared by plaintext

this could lead to confusing login/logout behavior
2025-09-03 19:50:54 +00:00
ed b59b915962 ie11 fixes 2025-09-03 19:48:47 +00:00
ed f0caf88185 add konmai quality
blame msw for this :p
2025-09-03 19:45:19 +00:00
ed bfcb6eac41 fix chrome reverting video pause toggles
pausing a video with spacebar while video is focused would first
get handled by the js hotkey, and then chrome would ignore our
hint that bubbling should cease and undo it anyways
2025-09-03 19:37:24 +00:00
ed e798a9a53a fix hotkeys on dvorak (closes #298, closes #733);
apparently the convention is that hotkeys should follow the letters
according to the layout, and not remain in the qwerty position

this breaks apart the cluster of media controls (uiojkl),
but that's the intended and expected behavior so it should be fine
2025-09-03 19:33:48 +00:00
xvrqt 09e3018bf9
nix-module: Add globalExtraConfig option (#751)
Added an option, 'services.copyparty.globalExtraConfig', with default
value and description to the NixOS Module. The option type is 'str' and
the default value is the empty string.

This string is appened verbatim to the [global] section of the config.
This allows the use of settings which rely on repeated values to be
correctly used. For example, the: 'ipu: 255.255.255.1/32=user' key which
allows automatic sign in for users of a CIDR subnet. Because attribute
sets in Nix must have unique keys, it is not possible to set more than
one CIDR subnet/user pair.

Signed-off-by: xvrqt <git@xvrqt.com>
2025-09-03 10:03:59 +02:00
ed 87539800e8 FTPS: add curl example (#734) 2025-08-31 19:06:56 +02:00
Christian Kastner 0469b5a29e bubbleparty.sh: process substitution requires bash
POSIX shell does not yet support  `<(process substitution)`.

Signed-off-by: Christian Kastner <ckk@kvr.at>
2025-08-31 18:44:39 +02:00
ed 3e90abbf6f update pkgs to 1.19.7 2025-08-28 20:24:59 +00:00
ed 26a29797a6 v1.19.7 2025-08-28 20:14:50 +00:00
ed 14555d5832 add chdir option 2025-08-28 20:14:25 +00:00
ed d1f75229b5 docker: ensure /state writable 2025-08-28 19:45:47 +00:00
ed 01cf20a029 docker: change $HOME to /state 2025-08-28 19:41:42 +00:00
EmilyxFox 6f0871173e
Set org.opencontainers.image.source label correctly in all dockerfiles (#717) 2025-08-28 21:07:42 +02:00
ed 914686ec7c fix navigation by holding I/K 2025-08-28 18:46:11 +02:00
ed 0d96786e68 fix using empty dir as state storage;
also supports 4111 (d--x--x--x) XDG_CONFIG_HOME
2025-08-28 18:15:37 +02:00
ed 4c3792de07 update pkgs to 1.19.6 2025-08-27 21:03:02 +00:00
ed 200eaa92d7 v1.19.6 2025-08-27 20:38:20 +00:00
ed d40f061a79 fix invalid up2k api usage 2025-08-27 20:15:45 +00:00
ed 28b93d7961 option to log invalid xml from clients (#695);
windows webdav can send invalid xml in LOCK requests
2025-08-27 19:55:21 +00:00
ed aa1c921302 support file-as-volume (#696);
a volume can be a single file instead of a folder,
but a misleading warning indicated otherwise
2025-08-27 19:17:36 +00:00
ed 2848941e01 more qr tweaks (closes #533);
* --qr-every draws the qr-code periodically, with/without --qr-pin
* --qr-winch redraws --qr-pin on console size change
* --qr-pin logs detected console size for debug
2025-08-27 19:01:19 +00:00
ed f4f702c39d shellcheck fixes; closes #690
a small selection of changes from the PR
2025-08-27 18:56:23 +00:00
ed 4c042b3c82 catch markdown table-fmt error; closes #699 2025-08-27 18:09:27 +00:00
ed 0b50fde305 audio-thumb as folder-thumb; closes #691 2025-08-27 18:03:10 +00:00
ed 0b3939002d helptext tweaks 2025-08-27 18:00:40 +00:00
ed 543b7ea959 add --auth-ord; closes #689 2025-08-26 23:33:53 +00:00
ed d30240b431 spaces in comma-sep. opts 2025-08-26 23:12:10 +00:00
ed 599e82f24d u2c: fix uploading files from root of unix fhs 2025-08-25 23:33:32 +00:00
ed abffda5474 unindent after mkdir(XDG_CONFIG_HOME) 2025-08-24 21:35:05 +00:00
ed 35472557cb strongly prefer XDG_CONFIG_HOME; closes #442 2025-08-24 21:34:37 +00:00
ed 48d6224ec8 url-param "dl" downloads file 2025-08-24 20:54:28 +00:00
ed 68503444c7 markdown: fix <code> in <a>; closes #552 2025-08-23 22:33:35 +00:00
ed 0491123bb2 add markdown newlines option (#552) 2025-08-23 22:29:41 +00:00
ed 8f235be66f enable polish translation 2025-08-23 21:31:30 +00:00
4ntsy 4b8c22159a
Portuguese translation (#673) 2025-08-23 21:28:37 +00:00
ed 59f142cd19 readme: u2c: rephrase no-hash warning 2025-08-23 19:58:33 +00:00
ed 6cd0a396df readme: clarify smb perf + u2c needs e2dsa (#678) 2025-08-23 19:45:18 +00:00
Gus P 6413ad3e8d fix LLM typo in CONTRIBUTING.md 2025-08-23 11:44:31 +02:00
nyqui ad0e6c7fde updated strings "mt_cflac", "mt_caac", "mt_coth" 2025-08-23 00:59:15 +02:00
ed 978801d020 ftp: fix link-local IPv6; closes #628 2025-08-22 22:26:51 +00:00
ed d39c74c126 fix fe80 assumption;
IPv6 link-local is fe80::/10, not just fe80
2025-08-22 21:43:31 +00:00
ed 5c250c2c19 update pkgs to 1.19.5 2025-08-21 23:41:08 +00:00
ed ceaf133d9d v1.19.5 2025-08-21 23:19:11 +00:00
ed 202ddeac0d write qrcode to file 2025-08-21 23:06:39 +00:00
ed cc4f4aef99 fix typos; closes #237, closes #465
correct subset of fixes, not touching the intentional/sacred ones
2025-08-21 22:03:34 +00:00
ed f9cb2c15e3 readme: homebrew 2025-08-21 21:50:56 +00:00
ed cc65b1b551 more helpful configparser 2025-08-21 21:26:13 +00:00
ed 6d76254c88 ftpd: fix ipv6 bonks (#628) 2025-08-21 20:28:21 +00:00
ed 0de07d8e8b fix a11y crash; closes #649 2025-08-21 19:39:48 +00:00
ed ca98d54fda install on iOS; closes #328 2025-08-21 19:23:44 +00:00
ed 6c76614eb1 fix rproxy hint; closes #661 2025-08-21 19:35:36 +02:00
ed 63d4ec64cd qr: fix fg=-1 with z=1 2025-08-21 19:14:45 +02:00
Daniel cd8771fa52
updated authelia docker-compose.yml (#377) 2025-08-20 23:30:26 +00:00
ed 15c5b50a36 versus.md: additional corrections 2025-08-20 23:17:01 +00:00
Massimo Melina 7a4973fa56 versus.md: corrections and updates for hfs3
Signed-off-by: Massimo Melina <a@rejetto.com>
2025-08-20 23:38:31 +02:00
Ruby Iris Juric 3259367007 readme: fix nixos overlay usage docs 2025-08-20 06:45:14 +02:00
DeStilleGast 5e36f02595 Update browser.js - Found a typo
Signed-off-by: DeStilleGast <3677706+DeStilleGast@users.noreply.github.com>
2025-08-19 23:26:06 +02:00
ed c51371c71d mention syncthing compat (#490, #199) 2025-08-18 23:08:49 +00:00
Ruby Iris Juric 20ef74cdac
nix: make usage in non-flake setups easier (#296)
* nix: extract overlay into own file
* readme: document non-flake nixos usage
2025-08-18 22:41:49 +00:00
ed b5c6b4fa99 update pkgs to 1.19.4 2025-08-17 16:11:06 +00:00
ed 377f7732de v1.19.4 2025-08-17 15:56:56 +00:00
ed b2fb0c26ad update pkgs to 1.19.3 2025-08-17 15:33:07 +00:00
ed da4ae66173 v1.19.3 2025-08-17 15:22:17 +00:00
ed bf1fdcab09 fix #556 fuckup, fixes #624 2025-08-17 15:19:25 +00:00
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
ed ddec22d04c v1.16.12 2025-02-09 23:31:26 +00:00
ed 32e90859f4 readme: add config file examples 2025-02-09 23:03:46 +00:00
ed 8b8970c787 add note about dead docker experiments 2025-02-09 21:17:25 +00:00
ed 03d35ba799 rename bubblewrap.sh to bubbleparty.sh (2/2)
reduces confusion if it ends up in $PATH, if there
are multiple such wrappers with different semantics
2025-02-09 20:40:16 +00:00
ed c035d7d88a rename bubblewrap.sh to bubbleparty.sh (1/2)
reduces confusion if it ends up in $PATH, if there
are multiple such wrappers with different semantics
2025-02-09 20:39:32 +00:00
ed 46f9e9efff add plugin: quickmove
defines hotkey W to move selected files into a subfolder
2025-02-09 19:40:36 +00:00
ed 4fa8d7ed79 hotkey S toggles selection of playing song
does not apply if image gallery is open

also ensure the hotkey handler is only attached once;
makes it easier to hook/modify it from plugins
2025-02-09 19:22:29 +00:00
ed cd71b505a9 safeguard against accidental config loss
when running copyparty without any config, it defaults to sharing
the current folder read-write for everyone. This makes sense for
quick one-off instances, but not in more permanent deployments

especially for docker, where the config can get lost by accident
in too many ways (compose typos, failed upgrade, selinux, ...)
the default should be to reject all access

add a safeguard which disables read-access if one or more
config-files were specified, but no volumes are defined

should prevent issues such as filebrowser/filebrowser#3719
2025-02-08 20:37:30 +00:00
ed c7db08ed3e remember file selection per-folder
avoids losing file selection when
accidentally changing to another folder
2025-02-08 15:18:07 +00:00
Leon van Kammen 3582a1004c added bubblewrap docs + script 2025-02-07 21:12:39 +01:00
ed 22cbd2dbb5 handlers: add http-redirect example 2025-02-07 19:03:13 +00:00
ed c87af9e85c option to restrict download-as-zip/tar
new global-option / volflag `zip_who` specifies
who gets to use the download-as-zip/tar function;

* 0: nobody, same as --no-zip
* 1: admins
* 2: authorized users with read-access
* 3: anyone with read-access
2025-02-05 20:45:50 +00:00
ed 6c202effa4 add plugin: graft-thumbs.js; #133
"sidecar thumbnails"; if a folder contains both foo.mp3 and foo.png
then this plugin takes the png thumbnail and applies to the mp3
while in the grid-view
2025-02-05 19:48:59 +00:00
ed 632f52af22 warn that RTL support is currently not planned
the current approach to html generation is
probably too jank to handle RTL correctly
2025-02-03 18:41:02 +00:00
ed 46e59529a4 ensure intended order in language selector 2025-02-03 18:32:58 +00:00
ed bdf060236a improve(?) bugreport template 2025-02-03 05:27:19 +00:00
ed d9d2a09282 mention fuse/rclone hijinks from #132 2025-02-02 23:22:06 +00:00
ed b020fd4ad2 make some 403s less ambiguous in logs 2025-02-02 23:02:54 +00:00
ed 4ef3526354 bbox: try to detect media load errors
listen for errors from <img> and <video> in the media gallery and
show an error-toast to indicate that the file isn't going to appear

unfortunately, when iOS-Safari fails to decode an unsupported video,
Safari itself appears to believe that everything is fine, and doesn't
issue the expected error-event, meaning we cannot detect this...

for example, trying to play non-yuv420p vp9 webm will silently fail,
with the only symptom being the play() promise throwing as the
<video> is destroyed during cleanup (bbox-close or media unload)
2025-01-31 21:13:35 +00:00
ed 20ddeb6e1b include last rtt in next req 2025-01-31 20:09:45 +00:00
ed d27f110498 http rtt in serverinfo panel 2025-01-31 20:00:33 +00:00
ed 910797ccb6 ping.html: add mdev, limit 2025-01-31 19:16:44 +00:00
ed 7de9d15aef add ping.html (from old php project) 2025-01-31 18:56:12 +00:00
ed 6a9ffe7e06 traefik-example: fix disconnect during big uploads
if an upload takes longer than 60 seconds,
by default, traefik closes the connection

thx to @JuvenoiaAgent@lemmy.ca for catching this
2025-01-29 21:03:18 +00:00
ed 12dcea4f70 improve iPad detection;
recent iPads do not indicate being an iPad in the user-agent,
so the audio-player would fall back on transcoding to mp3,
assuming the device cannot play opus-caf

improve this with pessimistic feature-detection for caf
hopefully still avoiding false-positives
2025-01-27 21:06:47 +00:00
ed b3b39bd8f1 update pkgs to 1.16.11 2025-01-27 02:01:25 +00:00
ed c7caecf77c v1.16.11 2025-01-27 01:40:23 +00:00
ed 1fe30363c7 u2c: option to print download links 2025-01-27 01:35:36 +00:00
ed 54a7256c8d fix js-panic if audio transcoding disabled 2025-01-27 00:37:03 +00:00
ed 8e8e4ff132 update pkgs to 1.16.10 2025-01-25 18:45:50 +00:00
236 changed files with 32136 additions and 5223 deletions

View file

@ -7,34 +7,48 @@ assignees: '9001'
---
NOTE:
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
<!-- 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 -->
**Describe the bug**
### Describe the bug
a description of what the bug is
**To Reproduce**
### To Reproduce
List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it
**Expected behavior**
### Expected behavior
a description of what you expected to happen
**Screenshots**
### Screenshots
if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)
**Server details**
if the issue is possibly on the server-side, then mention some of the following:
* server OS / version:
* python version:
* copyparty arguments:
* filesystem (`lsblk -f` on linux):
### Server details (if you are using docker/podman)
remove the ones that are not relevant:
* **server OS / version:**
* **how you're running copyparty:** (docker/podman/something-else)
* **docker image:** (variant, version, and arch if you know)
* **copyparty arguments and/or config-file:**
**Client details**
### 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/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:**
### Client details
if the issue is possibly on the client-side, then mention some of the following:
* the device type and model:
* OS version:
* browser version:
**Additional context**
### The rest of the stack
if you are connecting directly to copyparty then that's cool, otherwise please mention everything else between copyparty and the browser (reverseproxy, tunnels, etc.)
### Server log
if the issue might be server-related, include everything that appears in the copyparty log during startup, and also anything else you think might be relevant
### Additional context
any other context about the problem here

View file

@ -7,7 +7,9 @@ assignees: '9001'
---
all of the below are optional, consider them as inspiration, delete and rewrite at will
<!-- 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.**
a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`

4
.gitignore vendored
View file

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

52
.vscode/launch.json vendored
View file

@ -3,7 +3,7 @@
"configurations": [
{
"name": "Run copyparty",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "copyparty",
"console": "integratedTerminal",
@ -11,30 +11,46 @@
"justMyCode": false,
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"PYTHONWARNINGS": "always", //error
"PYTHONWARNINGS": "always" //error
},
"args": [
//"-nw",
"-ed",
"-emp",
//"-nw", // no-write; for testing uploads without writing to disk
//"-q", // quiet; speedboost when console output is not needed
// # increase debugger performance:
//"no-htp",
//"hash-mt=0",
//"mtag-mt=1",
//"th-mt=1",
// # listen for FTP and TFTP
"--ftp=3921",
"--ftp-pr=12000-12099",
"--tftp=3969",
// # listen on all IPv6, all IPv4, and unix-socket
"-i::,unix:777:a.sock",
// # misc
"--dedup",
"-e2dsa",
"-e2ts",
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
"--rss",
"--shr=/shr",
"--stats",
"-z",
// # users + volumes
"-aed:wark",
"-vsrv::r:rw,ed:c,dupe",
"-vdist:dist:r"
"-vdist:dist:r",
"-vsrv::r:rw,ed",
"-vsrv/junk:junk:r:A,ed",
"--ver"
]
},
{
"name": "No debug",
"preLaunchTask": "no_dbg",
"type": "python",
//"request": "attach", "port": 42069
// fork: nc -l 42069 </dev/null
},
{
"name": "Run active unit test",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "unittest",
"console": "integratedTerminal",
@ -51,6 +67,6 @@
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
},
}
]
}
}

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 / LLM when writing code
copyparty is 100% organic, free-range, human-written software!
> ⚠ you are now entering a no-copilot zone
the *only* place where LLM/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
@ -26,7 +39,9 @@ if you wanna have a go at coding it up yourself then maybe mention the idea on d
aside from documentation and ideas, some other things that would be cool to have some help with is:
* **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
* **translations** -- the copyparty web-UI has translations in [copyparty/web/tl](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl); if you'd like to [add a translation](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :>
* but please note that support for [RTL (Right-to-Left) languages](https://en.wikipedia.org/wiki/Right-to-left_script) is currently not planned, since the javascript is a bit too jank for that
* **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive.

779
README.md

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,7 @@
# Security Policy
if you hit something extra juicy pls let me know on either of the following
if you hit something extra juicy pls let me know on one of the following:
* email -- `copyparty@ocv.ze` except `ze` should be `me`
* [mastodon dm](https://layer8.space/@tripflag) -- `@tripflag@layer8.space`
* [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated
* [twitter dm](https://twitter.com/tripflag) (if im somehow not banned yet)
no bug bounties sorry! all i can offer is greetz in the release notes

View file

@ -1,7 +1,7 @@
# [`u2c.py`](u2c.py)
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* file uploads, file-search, autoresume of aborted/broken uploads
* sync local folder to server
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
* generally faster than browsers
* if something breaks just restart it
@ -78,3 +78,6 @@ cd /mnt/nas/music/.hist
# [`prisonparty.sh`](prisonparty.sh)
* run copyparty in a chroot, preventing any accidental file access
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
# [`bubbleparty.sh`](bubbleparty.sh)
* run copyparty in an isolated process, preventing any accidental file access and more

19
bin/bubbleparty.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
bwrap \
--unshare-all \
--ro-bind /usr /usr \
--ro-bind /bin /bin \
--ro-bind /lib /lib \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--dev-bind /dev /dev \
--dir /tmp \
--dir /var \
--bind "$(pwd)" "$(pwd)" \
--share-net \
--die-with-parent \
--file 11 /etc/passwd \
--file 12 /etc/group \
"$@" \
11< <(getent passwd $(id -u) 65534) \
12< <(getent group $(id -g) 65534)

View file

@ -8,7 +8,7 @@ import sqlite3
import argparse
DB_VER1 = 3
DB_VER2 = 5
DB_VER2 = 6
BY_PATH = None
NC = None
@ -39,7 +39,7 @@ def ls(db):
print(f"{nfiles} files")
print(f"{ntags} tags\n")
print("number of occurences for each tag,")
print("number of occurrences for each tag,")
print(" 'x' = file has no tags")
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
print()

View file

@ -20,6 +20,8 @@ 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"

52
bin/handlers/redirect.py Normal file
View file

@ -0,0 +1,52 @@
# if someone hits a 404, redirect them to another location
def send_http_302_temporary_redirect(cli, new_path):
"""
replies with an HTTP 302, which is a temporary redirect;
"new_path" can be any of the following:
- "http://a.com/" would redirect to another website,
- "/foo/bar" would redirect to /foo/bar on the same server;
note the leading '/' in the location which is important
"""
cli.reply(b"redirecting...", 302, headers={"Location": new_path})
def send_http_301_permanent_redirect(cli, new_path):
"""
replies with an HTTP 301, which is a permanent redirect;
otherwise identical to send_http_302_temporary_redirect
"""
cli.reply(b"redirecting...", 301, headers={"Location": new_path})
def send_errorpage_with_redirect_link(cli, new_path):
"""
replies with a website explaining that the page has moved;
"new_path" must be an absolute location on the same server
but without a leading '/', so for example "foo/bar"
would redirect to "/foo/bar"
"""
cli.redirect(new_path, click=False, msg="this page has moved")
def main(cli, vn, rem):
"""
this is the function that gets called by copyparty;
note that vn.vpath and cli.vpath does not have a leading '/'
so we're adding the slash in the debug messages below
"""
print(f"this client just hit a 404: {cli.ip}")
print(f"they were accessing this volume: /{vn.vpath}")
print(f"and the original request-path (straight from the URL) was /{cli.vpath}")
print(f"...which resolves to the following filesystem path: {vn.canonical(rem)}")
new_path = "/foo/bar/"
print(f"will now redirect the client to {new_path}")
# uncomment one of these:
send_http_302_temporary_redirect(cli, new_path)
# send_http_301_permanent_redirect(cli, new_path)
# send_errorpage_with_redirect_link(cli, new_path)
return "true"

View file

@ -4,6 +4,11 @@ these programs either take zero arguments, or a filepath (the affected file), or
run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban)
in particular, if a hook is loaded into copyparty with the hook-flag `c` ("check") then its exit-code controls the action that launched the hook:
* exit-code `0` = allow the action, and/or continue running the next hook
* exit-code `100` = allow the action, and stop running any remaining consecutive hooks
* anything else = reject/prevent the original action, and don't run the remaining hooks
> **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead
@ -14,6 +19,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,10 +32,20 @@ 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)
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
# on message
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
* [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
# general concept demos
* [import-me.py](import-me.py) shows how the `I` flag makes the hook 140x faster (but you need to be Very Careful when writing the plugin)
* [wget-i.py](wget-i.py) is an import-safe modification of [wget.py](wget.py)

55
bin/hooks/import-me.py Normal file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
from typing import Any
_ = r"""
the fastest hook in the west
(runs directly inside copyparty, not as a subprocess)
example usage as global config:
--xbu I,bin/hooks/import-me.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: I,bin/hooks/import-me.py
parameters explained,
I = import; do not fork / subprocess
IMPORTANT NOTE:
because this hook is running inside copyparty, you need to
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
DO NOT os.chdir() or anything like that, and also make sure
that the name of this file is unique (cannot be the same as
an existing python module/library)
"""
def main(ka: dict[str, Any]) -> dict[str, Any]:
# "ka" is a dictionary with info from copyparty...
# but because we are running inside copyparty, we don't need such courtesies;
import inspect
cf = inspect.currentframe().f_back.f_back.f_back
t = "hello from hook; I am able to peek into copyparty's memory like so:\n function name: %s\n variables:\n %s\n"
t2 = "\n ".join([("%r: %r" % (k, v))[:99] for k, v in cf.f_locals.items()][:9])
logger = ka["log"]
logger(t % (cf.f_code, t2))
# must return a dictionary with:
# "rc": the retcode; 0 is ok
return {"rc": 0}

View file

@ -9,7 +9,7 @@ from plyer import notification
_ = r"""
show os notification on upload; works on windows, linux, macos, android
depdencies:
dependencies:
windows: python3 -m pip install --user -U plyer
linux: python3 -m pip install --user -U plyer
macos: python3 -m pip install --user -U plyer pyobjus

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

@ -0,0 +1,60 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
_ = r"""
reject file upload (with a nice explanation why)
example usage as global config:
--xbu j,c1,bin/hooks/reject-and-explain.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: j,c1,bin/hooks/reject-and-explain.py
parameters explained,
xbu = execute-before-upload (can also be 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
"""
def main():
inf = json.loads(sys.argv[1])
vdir, fn = os.path.split(inf["vp"])
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
# the following is what decides if we'll accept the upload or reject it:
# we check if the upload-folder url matches the following regex-pattern:
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
if ok:
# allow the upload
print("{}")
return
# the upload was rejected; display the following errortext:
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
errmsg += inf["vp"] # if you want to mention the file's url
print(json.dumps({"rejectmsg": errmsg}))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3
import os
import threading
from argparse import Namespace
from jinja2.nodes import Name
from copyparty.fsutil import Fstab
from typing import Any, Optional
_ = r"""
reject an upload if the target folder is on a ramdisk; useful when you
have a volume where some folders inside are ramdisks but others aren't
example usage as global config:
--xbu I,bin/hooks/reject-ramdisk.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/reject-ramdisk.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: I,bin/hooks/reject-ramdisk.py
parameters explained,
I = import; do not fork / subprocess
IMPORTANT NOTE:
because this hook is imported inside copyparty, you need to
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
DO NOT os.chdir() or anything like that, and also make sure
that the name of this file is unique (cannot be the same as
an existing python module/library)
"""
mutex = threading.Lock()
fstab: Optional[Fstab] = None
def main(ka: dict[str, Any]) -> dict[str, Any]:
global fstab
with mutex:
log = ka["log"] # this is a copyparty NamedLogger function
if not fstab:
log("<HOOK:RAMDISK> creating fstab", 6)
args = Namespace()
args.mtab_age = 1 # cache the filesystem info for 1 sec
fstab = Fstab(log, args, False)
ap = ka["ap"] # abspath the upload is going to
fs, mp = fstab.get(ap) # figure out what the filesystem is
ramdisk = fs in ("tmpfs", "overlay") # looks like a ramdisk?
# log("<HOOK:RAMDISK> fs=%r" % (fs,))
if ramdisk:
t = "Upload REJECTED because destination is a ramdisk"
return {"rc": 1, "rejectmsg": t}
return {"rc": 0}

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);
}
var o = ebi(k);
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,33 @@ 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
msg = sys.argv[1]
if msg.startswith("upload-queue-empty;"):
return
label = msg.split(":usb-eject:")[1].split(":")[0]
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,))

100
bin/hooks/wget-i.py Executable file
View file

@ -0,0 +1,100 @@
#!/usr/bin/env python3
import os
import threading
import subprocess as sp
_ = r"""
use copyparty as a file downloader by POSTing URLs as
application/x-www-form-urlencoded (for example using the
📟 message-to-server-log in the web-ui)
this hook is a modified copy of wget.py, modified to
make it import-safe so it can be run with the 'I' flag,
which speeds up the startup time of the hook by 140x
example usage as global config:
--xm aw,I,bin/hooks/wget-i.py
parameters explained,
xm = execute on message-to-server-log
aw = only users with write-access can use this
I = import; do not fork / subprocess
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/wget.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all messages with the params explained above)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xm: aw,I,bin/hooks/wget.py
the volflag examples only kicks in if you send the message
while you're in the /inc folder (or any folder below there)
IMPORTANT NOTE:
because this hook uses the 'I' flag to run inside copyparty,
many other flags will not work (f,j,c3,t3600 as seen in the
original wget.py), and furthermore + more importantly we
need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so
the os.chdir has been replaced with cwd=dirpath for example
"""
def do_stuff(inf):
"""
worker function which is executed in another thread to
avoid blocking copyparty while the download is running,
since we cannot use the 'f,t3600' hook-flags with 'I'
"""
# first things first; grab the logger-function which copyparty is letting us borrow
log = inf["log"]
url = inf["txt"]
if url.startswith("upload-queue-empty;"):
return
if "://" not in url:
url = "https://" + url
proto = url.split("://")[0].lower()
if proto not in ("http", "https", "ftp", "ftps"):
raise Exception("bad proto {}".format(proto))
dirpath = inf["ap"]
name = url.split("?")[0].split("/")[-1]
msg = "-- DOWNLOADING " + name
log(msg)
tfn = os.path.join(dirpath, msg)
open(tfn, "wb").close()
cmd = ["wget", "--trust-server-names", "-nv", "--", url]
try:
# two things to note here:
# - cannot use the `c3` hook-flag with `I` so mute output with stdout=sp.DEVNULL instead;
# - MUST NOT use os.chdir with 'I' so use cwd=dirpath instead
sp.check_call(cmd, cwd=dirpath, stdout=sp.DEVNULL)
except:
t = "-- FAILED TO DOWNLOAD " + name
log(t, 3) # 3=yellow=warning
open(os.path.join(dirpath, t), "wb").close()
raise # have copyparty scream about the details in the log
os.unlink(tfn)
def main(inf):
threading.Thread(target=do_stuff, args=(inf,), daemon=True).start()

View file

@ -47,6 +47,9 @@ while you're in the /inc folder (or any folder below there)
def main():
inf = json.loads(sys.argv[1])
url = inf["txt"]
if url.startswith("upload-queue-empty;"):
return
if "://" not in url:
url = "https://" + url
@ -66,7 +69,7 @@ def main():
try:
sp.check_call(cmd)
except:
t = "-- FAILED TO DONWLOAD " + name
t = "-- FAILED TO DOWNLOAD " + name
print(f"{t}\n", end="")
open(t, "wb").close()

View file

@ -68,3 +68,8 @@ instead of affecting all volumes, you can set the options for just one volume li
* `:c,mtp=key=f,audio-key.py`
* `:c,mtp=.bpm=f,audio-bpm.py`
* `:c,mtp=ahash,vhash=f,media-hash.py`
# tips & tricks
* to delete tags for all files below `blog*` and rescan that, `sqlite3 .hist/up2k.db "delete from mt where w in (select substr(w,1,16) from up where rd like 'blog%')";`

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:

53
bin/mtag/geotag.py Executable file
View file

@ -0,0 +1,53 @@
import json
import re
import sys
from copyparty.util import fsenc, runcmd
"""
uses exiftool to geotag images based on embedded gps coordinates in exif data
adds four new metadata keys:
.gps_lat = latitute
.gps_lon = longitude
.masl = meters above sea level
city = "city, subregion, region"
usage: -mtp .masl,.gps_lat,.gps_lon,city=ad,t10,bin/mtag/geotag.py
example: https://a.ocv.me/pub/blog/j7/8/?grid=0
"""
def main():
cmd = b"exiftool -api geolocation -n".split(b" ")
rc, so, se = runcmd(cmd + [fsenc(sys.argv[1])])
ptn = re.compile("([^:]*[^ :]) *: (.*)")
city = ["", "", ""]
ret = {}
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
k, v = m.groups()
if k == "Geolocation City":
city[2] = v
elif k == "Geolocation Subregion":
city[1] = v
elif k == "Geolocation Region":
city[0] = v
elif k == "GPS Latitude":
ret[".gps_lat"] = "%.04f" % (float(v),)
elif k == "GPS Longitude":
ret[".gps_lon"] = "%.04f" % (float(v),)
elif k == "GPS Altitude":
ret[".masl"] = str(int(float(v)))
v = ", ".join(city).strip(", ")
if v:
ret["city"] = v
print(json.dumps(ret))
if __name__ == "__main__":
main()

View file

@ -7,7 +7,7 @@ example copyparty config to use this:
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
explained:
for realpath srv/hello (served at /hello), write-only for eveyrone,
for realpath srv/hello (served at /hello), write-only for everyone,
enable file analysis on upload (e2ts),
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
do this on all uploads regardless of extension,

View file

@ -11,7 +11,7 @@ example copyparty config to use this:
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
explained:
for realpath srv/hello (served at /hello),write-only for eveyrone,
for realpath srv/hello (served at /hello),write-only for everyone,
enable file analysis on upload (e2ts),
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
do this on all uploads with the file extension "bin",

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=
@ -186,12 +188,15 @@ install_keyfinder() {
echo "so not found at $sop"
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

@ -84,7 +84,7 @@ def main():
# on success, delete the .bin file which contains the URL
os.unlink(fp)
except:
open("-- FAILED TO DONWLOAD " + name, "wb").close()
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
os.unlink(tfn)
print(url)

View file

@ -6,8 +6,8 @@ __copyright__ = 2019
__license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/"
S_VERSION = "2.0"
S_BUILD_DT = "2024-10-01"
S_VERSION = "2.1"
S_BUILD_DT = "2025-09-06"
"""
mount a copyparty server (local or remote) as a filesystem
@ -99,7 +99,7 @@ except:
elif MACOS:
libfuse = "install https://osxfuse.github.io/"
else:
libfuse = "apt install libfuse3-3\n modprobe fuse"
libfuse = "apt install libfuse2\n modprobe fuse"
m = """\033[33m
could not import fuse; these may help:
@ -359,7 +359,7 @@ class Gateway(object):
def sendreq(self, meth, path, headers, **kwargs):
tid = get_tid()
if self.password:
headers["Cookie"] = "=".join(["cppwd", self.password])
headers["PW"] = self.password
try:
c = self.getconn(tid)
@ -902,9 +902,7 @@ class CPPF(Operations):
return ret
def _readdir(self, path, fh=None):
path = path.strip("/")
dbg("readdir %r [%s]", path, fh)
dbg("dircache miss")
ret = self.gw.listdir(path)
if not self.n_dircache:
return ret
@ -914,11 +912,17 @@ class CPPF(Operations):
self.dircache.append(cn)
self.clean_dircache()
# import pprint; pprint.pprint(ret)
return ret
def readdir(self, path, fh=None):
return [".", ".."] + list(self._readdir(path, fh))
dbg("readdir %r [%s]", path, fh)
path = path.strip("/")
cn = self.get_cached_dir(path)
if cn:
ret = cn.data
else:
ret = self._readdir(path, fh)
return [".", ".."] + list(ret)
def read(self, path, length, offset, fh=None):
req_max = 1024 * 1024 * 8
@ -993,7 +997,6 @@ class CPPF(Operations):
if cn:
dents = cn.data
else:
dbg("cache miss")
dents = self._readdir(dirpath)
try:
@ -1141,10 +1144,15 @@ def main():
if WINDOWS:
examples.append("http://192.168.1.69:3923/music/ M:")
epi = "example:" + ex_pre + ex_pre.join(examples)
epi += """\n
NOTE: if server has --usernames enabled, then password is "username:password"
"""
ap = argparse.ArgumentParser(
formatter_class=TheArgparseFormatter,
description="mount a copyparty server as a local filesystem -- " + ver,
epilog="example:" + ex_pre + ex_pre.join(examples),
epilog=epi,
)
# fmt: off
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")

View file

@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
# create a dev
(cd $jail; mkdir -p dev; cd dev
(cd "$jail"; mkdir -p dev; cd dev
[ -e null ] || mknod -m 666 null c 1 3
[ -e zero ] || mknod -m 666 zero c 1 5
[ -e random ] || mknod -m 444 random c 1 8

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "2.8"
S_BUILD_DT = "2025-01-21"
S_VERSION = "2.15"
S_BUILD_DT = "2025-10-25"
"""
u2c.py: upload to copyparty
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
- dependencies: no
- supports python 2.6, 2.7, and 3.3 through 3.12
- supports python 2.6, 2.7, and 3.3 through 3.14
- if something breaks just try again and it'll autoresume
"""
@ -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"
@ -230,10 +232,15 @@ class HCli(object):
MJ = "application/json"
MO = "application/octet-stream"
MM = "application/x-www-form-urlencoded"
CLEN = "Content-Length"
web = None # type: HCli
links = [] # type: list[str]
linkmtx = threading.Lock()
linkfile = None
class File(object):
"""an up2k upload task; represents a single file"""
@ -584,9 +591,10 @@ def undns(url):
def _scd(err, top):
"""non-recursive listing of directory contents, along with stat() info"""
top_ = os.path.join(top, b"")
with os.scandir(top) as dh:
for fh in dh:
abspath = os.path.join(top, fh.name)
abspath = top_ + fh.name
try:
yield [abspath, fh.stat()]
except Exception as ex:
@ -595,8 +603,9 @@ def _scd(err, top):
def _lsd(err, top):
"""non-recursive listing of directory contents, along with stat() info"""
top_ = os.path.join(top, b"")
for name in os.listdir(top):
abspath = os.path.join(top, name)
abspath = top_ + name
try:
yield [abspath, os.stat(abspath)]
except Exception as ex:
@ -671,7 +680,7 @@ def walkdirs(err, tops, excl):
yield stop, ap[len(stop) :].lstrip(sep), inf
else:
d, n = top.rsplit(sep, 1)
yield d, n, os.stat(top)
yield d or b"/", n, os.stat(top)
# mostly from copyparty/util.py
@ -761,6 +770,29 @@ def get_hashlist(file, pcb, mth):
file.kchunks[k] = [v1, v2]
def printlink(ar, purl, name, fk):
if not name:
url = purl # srch
else:
name = quotep(name.encode("utf-8", WTF8)).decode("utf-8")
if fk:
url = "%s%s?k=%s" % (purl, name, fk)
else:
url = "%s%s" % (purl, name)
url = "%s/%s" % (ar.burl, url.lstrip("/"))
with linkmtx:
if ar.u:
links.append(url)
if ar.ud:
print(url)
if linkfile:
zs = "%s\n" % (url,)
zb = zs.encode("utf-8", "replace")
linkfile.write(zb)
def handshake(ar, file, search):
# type: (argparse.Namespace, File, bool) -> tuple[list[str], bool]
"""
@ -780,7 +812,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
@ -832,12 +866,17 @@ def handshake(ar, file, search):
raise Exception(txt)
if search:
if ar.uon and r["hits"]:
printlink(ar, r["hits"][0]["rp"], "", "")
return r["hits"], False
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
file.name = r["name"]
file.wark = r["wark"]
if ar.uon and not r["hash"]:
printlink(ar, file.url, r["name"], r.get("fk"))
return r["hash"], r["sprs"]
@ -941,6 +980,7 @@ class Ctl(object):
self.nfiles, self.nbytes = self.stats
self.filegen = walkdirs([], ar.files, ar.x)
self.recheck = [] # type: list[File]
self.last_file = None
if ar.safe:
self._safe()
@ -977,6 +1017,11 @@ class Ctl(object):
self._fancy()
file = self.last_file
if self.up_br and file:
zs = quotep(file.name.encode("utf-8", WTF8))
web.req("POST", file.url, {}, b"msg=upload-queue-empty;" + zs, MM)
self.ok = not self.errs
def _safe(self):
@ -1187,9 +1232,7 @@ class Ctl(object):
while req:
print("DELETING ~%s#%s" % (srd, len(req)))
body = json.dumps(req).encode("utf-8")
sc, txt = web.req(
"POST", self.ar.url + "?delete", {}, body, MJ
)
sc, txt = web.req("POST", "/?delete", {}, body, MJ)
if sc == 413 and "json 2big" in txt:
print(" (delete request too big; slicing...)")
req = req[: len(req) // 2]
@ -1255,7 +1298,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,
@ -1417,6 +1460,7 @@ class Ctl(object):
file = fsl.file
cids = fsl.cids
self.last_file = file
with self.mutex:
if not self.uploader_busy:
@ -1472,7 +1516,7 @@ class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFor
def main():
global web
global web, linkfile
time.strptime("19970815", "%Y%m%d") # python#7980
"".encode("idna") # python#29288
@ -1491,14 +1535,14 @@ def main():
# fmt: off
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
NOTE:
source file/folder selection uses rsync syntax, meaning that:
NOTE: source file/folder selection uses rsync syntax, meaning that:
"foo" uploads the entire folder to URL/foo/
"foo/" uploads the CONTENTS of the folder into URL/
NOTE: if server has --usernames enabled, then password is "username:password"
""")
ap.add_argument("url", type=unicode, help="server url, including destination folder")
ap.add_argument("files", type=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)")
@ -1506,9 +1550,15 @@ 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")
ap = app.add_argument_group("print links")
ap.add_argument("-u", action="store_true", help="print list of download-links after all uploads finished")
ap.add_argument("-ud", action="store_true", help="print download-link after each upload finishes")
ap.add_argument("-uf", type=unicode, metavar="PATH", help="print list of download-links to file")
ap = app.add_argument_group("compatibility")
ap.add_argument("--cls", action="store_true", help="clear screen before start")
ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)")
@ -1594,6 +1644,10 @@ source file/folder selection uses rsync syntax, meaning that:
ar.x = "|".join(ar.x or [])
setattr(ar, "wlist", ar.url == "-")
setattr(ar, "uon", ar.u or ar.ud or ar.uf)
if ar.uf:
linkfile = open(ar.uf, "wb")
for k in "dl dr drd wlist".split():
errs = []
@ -1656,6 +1710,12 @@ source file/folder selection uses rsync syntax, meaning that:
ar.z = True
ctl = Ctl(ar, ctl.stats)
if links:
print()
print("\n".join(links))
if linkfile:
linkfile.close()
if ctl.errs:
print("WARNING: %d errors" % (ctl.errs))

View file

@ -61,6 +61,8 @@ def rep_server():
print("copyparty says %r" % (sck.recv_string(),))
reply = b"thx"
# reply = b"return 1" # non-zero to block an upload
# reply = b'{"rc":1}' # or as json, that's fine too
# reply = b'{"rejectmsg":"naw dude"}' # or custom message
sck.send(reply)

View file

@ -7,6 +7,12 @@
* works on windows, linux and macos
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
### [`setup-ashell.sh`](setup-ashell.sh)
* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
* not very useful due to limitations in iOS:
* not able to share all of your phone's storage
* cannot run in the background
### [`index.html`](index.html)
* drop-in redirect from an httpd to copyparty
* assumes the webserver and copyparty is running on the same server/IP
@ -50,6 +56,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
# and then enable it below by uncommenting 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,23 +1,28 @@
{ 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
# sets with a true boolean value are coerced to just the key name
key
else if value == false then
# or omitted completely when false
# or omitted completely when false
""
else
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
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
@ -43,19 +48,24 @@ let
accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);
volumesWithoutVariables = filterAttrs (k: v: !(hasInfix "\${" v.path)) cfg.volumes;
configStr = ''
${mkSection "global" cfg.settings}
${cfg.globalExtraConfig}
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
${mkSection "groups" cfg.groups}
${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 +78,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,33 +118,47 @@ 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};
}
'';
};
globalExtraConfig = mkOption {
type = types.str;
default = "";
description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set.";
};
accounts = mkOption {
type = types.attrsOf (types.submodule ({ ... }: {
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Runtime file path to a file containing the user password.
Must be readable by the copyparty user.
'';
example = "/run/keys/copyparty/ed";
};
};
}));
type = types.attrsOf (
types.submodule (
{ ... }:
{
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Runtime file path to a file containing the user password.
Must be readable by the copyparty user.
'';
example = "/run/keys/copyparty/ed";
};
};
}
)
);
description = ''
A set of copyparty accounts to create.
'';
@ -117,75 +170,95 @@ in {
'';
};
volumes = mkOption {
type = types.attrsOf (types.submodule ({ ... }: {
options = {
path = mkOption {
type = types.str;
description = ''
Path of a directory to share.
'';
};
access = mkOption {
type = types.attrs;
description = ''
Attribute list of permissions and the users to apply them to.
The key must be a string containing any combination of allowed permission:
"r" (read): list folder contents, download files
"w" (write): upload files; need "r" to see the uploads
"m" (move): move files and folders; need "w" at destination
"d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads
"h" (html): "get", but folders return their index.html
"a" (admin): can see uploader IPs, config-reload
For example: "rwmd"
The value must be one of:
an account name, defined in `accounts`
a list of account names
"*", which means "any account"
'';
example = literalExpression ''
{
# wG = write-upget = see your own uploads only
wG = "*";
# read-write-modify-delete for users "ed" and "k"
rwmd = ["ed" "k"];
};
'';
};
flags = mkOption {
type = types.attrs;
description = ''
Attribute list of volume flags to apply.
See `${getExe cfg.package} --help-flags` for more details.
'';
example = literalExpression ''
{
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
fk = 4;
# scan for new files every 60sec
scan = 60;
# volflag "e2d" enables the uploads database
e2d = true;
# "d2t" disables multimedia parsers (in case the uploads are malicious)
d2t = true;
# skips hashing file contents if path matches *.iso
nohash = "\.iso$";
};
'';
default = { };
};
groups = mkOption {
type = types.attrsOf (types.listOf types.str);
description = ''
A set of copyparty groups to create and the users that should be part of each group.
'';
default = { };
example = literalExpression ''
{
group_name = [ "user1" "user2" ];
};
}));
'';
};
volumes = mkOption {
type = types.attrsOf (
types.submodule (
{ ... }:
{
options = {
path = mkOption {
type = types.path;
description = ''
Path of a directory to share.
'';
};
access = mkOption {
type = types.attrs;
description = ''
Attribute list of permissions and the users to apply them to.
The key must be a string containing any combination of allowed permission:
"r" (read): list folder contents, download files
"w" (write): upload files; need "r" to see the uploads
"m" (move): move files and folders; need "w" at destination
"d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads
"h" (html): "get", but folders return their index.html
"a" (admin): can see uploader IPs, config-reload
For example: "rwmd"
The value must be one of:
an account name, defined in `accounts`
a list of account names
"*", which means "any account"
'';
example = literalExpression ''
{
# wG = write-upget = see your own uploads only
wG = "*";
# read-write-modify-delete for users "ed" and "k"
rwmd = ["ed" "k"];
};
'';
};
flags = mkOption {
type = types.attrs;
description = ''
Attribute list of volume flags to apply.
See `${getExe cfg.package} --help-flags` for more details.
'';
example = literalExpression ''
{
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
fk = 4;
# scan for new files every 60sec
scan = 60;
# volflag "e2d" enables the uploads database
e2d = true;
# "d2t" disables multimedia parsers (in case the uploads are malicious)
d2t = true;
# skips hashing file contents if path matches *.iso
nohash = "\.iso$";
};
'';
default = { };
};
};
}
)
);
description = "A set of copyparty volumes to create";
default = {
"/" = {
path = defaultShareDir;
access = { r = "*"; };
access = {
r = "*";
};
};
};
example = literalExpression ''
@ -204,80 +277,136 @@ in {
};
};
config = mkIf cfg.enable {
systemd.services.copyparty = {
description = "http file sharing hub";
wantedBy = [ "multi-user.target" ];
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";
environment = {
PYTHONUNBUFFERED = "true";
XDG_CONFIG_HOME = externalStateDir;
};
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)}
'';
serviceConfig = {
Type = "simple";
ExecStart = command;
# Hardening options
User = cfg.user;
Group = cfg.group;
RuntimeDirectory = [ "copyparty" ];
RuntimeDirectoryMode = "0700";
StateDirectory = [ "copyparty" ];
StateDirectoryMode = "0700";
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 =
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
++ [ externalStateDir ]
++ (mapAttrsToList (k: v: v.path) volumesWithoutVariables);
# 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;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectHostname = true;
ProtectClock = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RemoveIPC = true;
UMask = "0077";
LimitNOFILE = cfg.openFilesLimit;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
};
};
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)}
'';
# 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 = ":${
# Use volume permissions if set
if (value.flags ? chmod_d) then
value.flags.chmod_d
# Else, use global permission if set
else if (cfg.settings ? chmod-d) then
cfg.settings.chmod-d
# Else, use the default permission
else
"755"
}";
};
}
) volumesWithoutVariables
);
serviceConfig = {
Type = "simple";
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
# Hardening options
User = "copyparty";
Group = "copyparty";
RuntimeDirectory = name;
RuntimeDirectoryMode = "0700";
StateDirectory = [ name "${name}/data" "${name}/.config" ];
StateDirectoryMode = "0700";
WorkingDirectory = home;
TemporaryFileSystem = "/:ro";
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/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;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectHostname = true;
ProtectClock = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RemoveIPC = true;
UMask = "0077";
LimitNOFILE = cfg.openFilesLimit;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
users.groups = lib.mkIf (cfg.group == "copyparty") {
copyparty = { };
};
};
users.users = lib.mkIf (cfg.user == "copyparty") {
copyparty = {
description = "Service user for copyparty";
group = cfg.group;
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
users.groups.copyparty = { };
users.users.copyparty = {
description = "Service user for copyparty";
group = "copyparty";
home = home;
isSystemUser = true;
};
};
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.9"
pkgver="1.19.21"
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)"
"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"
"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)"
"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: detection of musical keys"
"python-pyopenssl: ftps functionality"
"python-pyzmq: send zeromq messages from event-hooks"
"python-argon2-cffi: hashed passwords in config"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("3e8f3c24c699aa41e0d51db6d781e453979c77abc34c919063b5bddd64d27bb0")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42")
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.21
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=("44723a823f218e52aaec6075695973a75b8663c9202c80fd73f48e52c61acd42")
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

@ -26,7 +26,7 @@ Environment=XDG_CONFIG_HOME=/home/cpp/.config
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# run copyparty
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init
[Install]
WantedBy=multi-user.target

View file

@ -1,45 +1,139 @@
{ 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: [ ]),
# to build stable + unstable with the same file
stable ? true,
# for commit date, only used when stable = false
copypartyFlake ? null,
nix-gitignore,
}:
let
pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (ps:
with ps; [
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
inherit (copypartyFlake) lastModifiedDate;
# ex: "1970" "01" "01"
dateStringsZeroPrefixed = {
year = builtins.substring 0 4 lastModifiedDate;
month = builtins.substring 4 2 lastModifiedDate;
day = builtins.substring 6 2 lastModifiedDate;
};
# ex: "1970" "1" "1"
dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;
unstableVersion =
if copypartyFlake == null then
"${pinData.version}-unstable"
else
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}"
;
version = if stable then pinData.version else unstableVersion;
stableSrc = fetchurl {
inherit (pinData) url hash;
};
root = ../../../..;
unstableSrc = nix-gitignore.gitignoreSource [] root;
src = if stable then stableSrc else unstableSrc;
rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown";
unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}");
in
buildPythonApplication {
pname = "copyparty";
inherit version src;
postPatch = lib.optionalString (!stable) ''
old_src="$(mktemp -d)"
tar -C "$old_src" -xf ${stableSrc}
declare -a folders
folders=("$old_src"/*)
count_folders="''${#folders[@]}"
if [[ $count_folders != 1 ]]; then
declare -p folders
echo "Expected 1 folder, found $count_folders" >&2
exit 1
fi
old_src_folder="''${folders[0]}"
cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps
sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py
${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
'')}
'';
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 +141,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;
++ 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 ];
};
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"
'';
}

View file

@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.16.9/copyparty-sfx.py",
"version": "1.16.9",
"hash": "sha256-456L3IHzf8ups3L9pTBZJMQjML8AlsQI66HZohDyEIA="
"url": "https://github.com/9001/copyparty/releases/download/v1.19.21/copyparty-1.19.21.tar.gz",
"version": "1.19.21",
"hash": "sha256-RHI6gj8hjlKq7GB1aVlzp1uGY8kgLID9c/SOUsYazUI="
}

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,31 @@
final: prev:
let
fullAttrs = {
withHashedPasswords = true;
withCertgen = true;
withThumbnails = true;
withFastThumbnails = true;
withMediaProcessing = true;
withBasicAudioMetadata = true;
withZeroMQ = true;
withFTP = true;
withFTPS = true;
withTFTP = true;
withSMB = true;
withMagic = true;
};
call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs);
in
{
copyparty = call { stable = true; };
copyparty-unstable = call { stable = false; };
copyparty-full = call (fullAttrs // { stable = true; });
copyparty-unstable-full = call (fullAttrs // { stable = false; });
python3 = prev.python3.override {
packageOverrides = pyFinal: pyPrev: {
partftpy = pyFinal.callPackage ./partftpy { };
};
};
}

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

@ -15,6 +15,7 @@ save one of these as `.epilogue.html` inside a folder to customize it:
point `--js-browser` to one of these by URL:
* [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders
* [`quickmove.js`](quickmove.js) adds a hotkey to move selected files into a subfolder
* [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading
* [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API
@ -38,3 +39,9 @@ point `--css-browser` to one of these by URL:
* turns copyparty into chromecast just more flexible (and probably way more buggy)
* usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js`
# junk
* [**rave.js**](./rave.js): april-fools joke, [demo (epilepsy warning)](https://cd.ocv.me/b/d2/d21/#af-9b927c42,sorthref), not maintained, very buggy

View file

@ -0,0 +1,117 @@
// USAGE:
// place this file somewhere in the webroot and then
// python3 -m copyparty --js-browser /.res/graft-thumbs.js
//
// DESCRIPTION:
// this is a gridview plugin which, for each file in a folder,
// looks for another file with the same filename (but with a
// different file extension)
//
// if one of those files is an image and the other is not,
// then this plugin assumes the image is a "sidecar thumbnail"
// for the other file, and it will graft the image thumbnail
// onto the non-image file (for example an mp3)
//
// optional feature 1, default-enabled:
// the image-file is then hidden from the directory listing
//
// optional feature 2, default-enabled:
// when clicking the audio file, the image will also open
(function() {
// `graft_thumbs` assumes the gridview has just been rendered;
// it looks for sidecars, and transplants those thumbnails onto
// the other file with the same basename (filename sans extension)
var graft_thumbs = function () {
if (!thegrid.en)
return; // not in grid mode
var files = msel.getall(),
pairs = {};
console.log(files);
for (var a = 0; a < files.length; a++) {
var file = files[a],
is_pic = /\.(jpe?g|png|gif|webp)$/i.exec(file.vp),
is_audio = re_au_all.exec(file.vp),
basename = file.vp.replace(/\.[^\.]+$/, ""),
entry = pairs[basename];
if (!entry)
// first time seeing this basename; create a new entry in pairs
entry = pairs[basename] = {};
if (is_pic)
entry.thumb = file;
else if (is_audio)
entry.audio = file;
}
var basenames = Object.keys(pairs);
for (var a = 0; a < basenames.length; a++)
(function(a) {
var pair = pairs[basenames[a]];
if (!pair.thumb || !pair.audio)
return; // not a matching pair of files
var img_thumb = QS('#ggrid a[ref="' + pair.thumb.id + '"] img[onload]'),
img_audio = QS('#ggrid a[ref="' + pair.audio.id + '"] img[onload]');
if (!img_thumb || !img_audio)
return; // something's wrong... let's bail
// alright, graft the thumb...
img_audio.src = img_thumb.src;
// ...and hide the sidecar
img_thumb.closest('a').style.display = 'none';
// ...and add another onclick-handler to the audio,
// so it also opens the pic while playing the song
img_audio.addEventListener('click', function() {
img_thumb.click();
return false; // let it bubble to the next listener
});
})(a);
};
// ...and then the trick! near the end of loadgrid,
// thegrid.bagit is called to initialize the baguettebox
// (image/video gallery); this is the perfect function to
// "hook" (hijack) so we can run our code :^)
// need to grab a backup of the original function first,
var orig_func = thegrid.bagit;
// and then replace it with our own:
thegrid.bagit = function (isrc) {
if (isrc !== '#ggrid')
// we only want to modify the grid, so
// let the original function handle this one
return orig_func(isrc);
graft_thumbs();
// when changing directories, the grid is
// rendered before msel returns the correct
// filenames, so schedule another run:
setTimeout(graft_thumbs, 1);
// and finally, call the original thegrid.bagit function
return orig_func(isrc);
};
if (ls0) {
// the server included an initial listing json (ls0),
// so the grid has already been rendered without our hook
graft_thumbs();
}
})();

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,140 @@
"use strict";
// USAGE:
// place this file somewhere in the webroot,
// for example in a folder named ".res" to hide it, and then
// python3 copyparty-sfx.py -v .::A --js-browser /.res/quickmove.js
//
// DESCRIPTION:
// the command above launches copyparty with one single volume;
// ".::A" = current folder as webroot, and everyone has Admin
//
// the plugin adds hotkey "W" which moves all selected files
// into a subfolder named "foobar" inside the current folder
(function() {
var action_to_perform = ask_for_confirmation_and_then_move;
// this decides what the new hotkey should do;
// ask_for_confirmation_and_then_move = show a yes/no box,
// move_selected_files = just move the files immediately
var move_destination = "foobar";
// this is the target folder to move files to;
// by default it is a subfolder of the current folder,
// but it can also be an absolute path like "/foo/bar"
// ===
// === END OF CONFIG
// ===
var main_hotkey_handler, // copyparty's original hotkey handler
plugin_enabler, // timer to engage this plugin when safe
files_to_move; // list of files to move
function ask_for_confirmation_and_then_move() {
var num_files = msel.getsel().length,
msg = "move the selected " + num_files + " files?";
if (!num_files)
return toast.warn(2, 'no files were selected to be moved');
modal.confirm(msg, move_selected_files, null);
}
function move_selected_files() {
var selection = msel.getsel();
if (!selection.length)
return toast.warn(2, 'no files were selected to be moved');
if (thegrid.bbox) {
// close image/video viewer
thegrid.bbox = null;
baguetteBox.destroy();
}
files_to_move = [];
for (var a = 0; a < selection.length; a++)
files_to_move.push(selection[a].vp);
move_next_file();
}
function move_next_file() {
var num_files = files_to_move.length,
filepath = files_to_move.pop(),
filename = vsplit(filepath)[1];
toast.inf(10, "moving " + num_files + " files...\n\n" + filename);
var dst = move_destination;
if (!dst.endsWith('/'))
// must have a trailing slash, so add it
dst += '/';
if (!dst.startsWith('/'))
// destination is a relative path, so prefix current folder path
dst = get_evpath() + dst;
// and finally append the filename
dst += '/' + filename;
// prepare the move-request to be sent
var xhr = new XHR();
xhr.onload = xhr.onerror = function() {
if (this.status !== 201)
return toast.err(30, 'move failed: ' + esc(this.responseText));
if (files_to_move.length)
return move_next_file(); // still more files to go
toast.ok(1, 'move OK');
treectl.goto(); // reload the folder contents
};
xhr.open('POST', filepath + '?move=' + dst);
xhr.send();
}
function our_hotkey_handler(e) {
// bail if either ALT, CTRL, or SHIFT is pressed
if (anymod(e))
return main_hotkey_handler(e); // let copyparty handle this keystroke
var keycode = (e.key || e.code) + '',
ae = document.activeElement,
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
// check the current aet (active element type),
// only continue if one of the following currently has input focus:
// nothing | link | button | table-row | table-cell | div | text
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
return main_hotkey_handler(e); // let copyparty handle this keystroke
if (keycode == 'w' || keycode == 'KeyW') {
// okay, this one's for us... do the thing
action_to_perform();
return ev(e);
}
return main_hotkey_handler(e); // let copyparty handle this keystroke
}
function enable_plugin() {
if (!window.hotkeys_attached)
return console.log('quickmove is waiting for the page to finish loading');
clearInterval(plugin_enabler);
main_hotkey_handler = document.onkeydown;
document.onkeydown = our_hotkey_handler;
console.log('quickmove is now enabled');
}
// copyparty doesn't enable its hotkeys until the page
// has finished loading, so we'll wait for that too
plugin_enabler = setInterval(enable_plugin, 100);
})();

View file

@ -0,0 +1,173 @@
# copyparty with Podman and Systemd
Use this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service.
Documentation for `.container` files can be found in the [Container unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container) docs. Systemd does not understand `.container` files natively, so Podman converts these to `.service` files with a [systemd-generator](https://www.freedesktop.org/software/systemd/man/latest/systemd.generator.html). This process is transparent, but sometimes needs to be debugged in case your `.container` file is malformed. There are instructions to debug the systemd generator in the Troubleshooting section below.
To run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation
There is a sample configuration file in the same directory as this file (`copyparty.conf`).
## Run the container as root
Running the container as the root user is easy to set up, but less secure. There are instructions in the next section to run the container as a rootless user if you'd rather run the container like that.
First, change this line in the `copyparty.container` file to reflect the directory you want to share. By default, it shares `/mnt/` but you'll probably want to change that.
```
# Change /mnt to something you want to share
Volume=/mnt:/w:z
```
Note that you can select the owner and group of this volume by changing the `uid:` and `gid:` of the volume in `copyparty.conf`, but for simplicity let's assume you want it to be owned by `root:root`.
To install and start copyparty with Podman and systemd as the root user, run the following:
```shell
sudo mkdir -pv /etc/systemd/container/ /etc/copyparty/
sudo cp -v copyparty.container /etc/systemd/containers/
sudo cp -v copyparty.conf /etc/copyparty/
sudo systemctl daemon-reload
sudo systemctl start copyparty
```
Note: You can't "enable" this kind of Podman service. The `[Install]` section of the `.container` file effectively handles enabling the service so that it starts when the server reboots.
You can see the status of the service with:
```shell
sudo systemctl status -a copyparty
```
You can see (and follow) the logs with either of these commands:
```shell
sudo podman logs -f copyparty
# -a is required or else you'll get output like: copyparty[549025]: [649B blob data]
sudo journalctl -a -f -u copyparty
```
## Run the container as a non-root user
This configuration is more secure, but is more involved and requires ensuring files have proper permissions. You will need a root user account to do some of this setup.
First, you need a user to run the container as. In this example we'll create a "podman" user with UID=1001 and GID=1001.
```shell
sudo groupadd -g 1001 podman
sudo useradd -u 1001 -m podman
sudo usermod -aG podman podman
sudo loginctl enable-linger podman
# Set a strong password for this user
sudo -u podman passwd
```
The `enable-linger` command allows the podman user to run systemd user services that persist even when the user is not logged in. You could use a user that already exists in the system to run this service as, just make sure to run `loginctl enable-linger USERNAME` for that user.
Next, change these lines in the `copyparty.container` file to reflect the config directory and the directory you want to share. By default, the config shares `/home/podman/copyparty/sharing/` but you'll probably want to change this:
```
# Change to reflect your non-root user's home directory
Volume=/home/podman/copyparty/config:/cfg:z
# Change to the directory you want to share
Volume=/home/podman/copyparty/sharing:/w:z
```
Make sure the podman user has read/write access to both of these directories.
Next, **log in to the server as the podman user**.
To install and start copyparty as the non-root podman user, run the following:
```shell
mkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config
cp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container
cp -v copyparty.conf /home/podman/copyparty/config
systemctl --user daemon-reload
systemctl --user start copyparty
```
**Important note: Never use `sudo` with `systemctl --user`!**
You can check the status of the user service with:
```shell
systemctl --user status -a copyparty
```
You can see (and follow) the logs with:
```shell
podman logs -f copyparty
journalctl --user -a -f -u copyparty
```
## Troubleshooting
If the container fails to start, and you've modified the `.container` service, it's likely that your `.container` file failed to be translated into a `.service` file. You can debug the podman service generator with this command:
```shell
sudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun
```
## Allowing Traffic from Outside your Server
To allow traffic on port 3923 of your server, you should run:
```shell
sudo firewall-cmd --permanent --add-port=3923/tcp
sudo firewall-cmd --reload
```
Otherwise, you won't be able to access the copyparty server from anywhere other than the server itself.
## Updating copyparty
To update the version of copyparty used in the container, you can:
```shell
# If root:
sudo podman pull docker.io/copyparty/ac:latest
sudo systemctl restart copyparty
# If non-root:
podman pull docker.io/copyparty/ac:latest
systemctl --user restart copyparty
```
Or, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run:
```shell
# If root:
sudo systemctl daemon-reload
sudo systemctl restart copyparty
# If non-root:
systemctl --user daemon-reload
systemctl --user restart copyparty
```
Podman will pull the image you've specified when restarting. If you have it set to `:latest`, Podman does not know to re-pull the container.
### Enabling auto-update
Alternatively, you can enable auto-updates by un-commenting this line:
```
# AutoUpdate=registry
```
You will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with:
```shell
# If root:
sudo systemctl enable podman-auto-update.timer podman-auto-update.service
# If non-root:
systemctl --user enable podman-auto-update.timer podman-auto-update.service
```
This works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours.

View file

@ -0,0 +1,36 @@
[global]
e2dsa # enable file indexing and filesystem scanning
e2ts # and enable multimedia indexing
ansi # and colors in log messages
# uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl;
# $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
/w # share the contents of the "/w" folder
accs:
rw: * # everyone gets read-write access, but
rwmda: ed # the user "ed" gets read-write-move-delete-admin
# uid: 1000 # If you're running as root, you can change the owner of this volume here
# gid: 1000 # If you're running as root, you can change the group of this volume here

View file

@ -0,0 +1,55 @@
[Container]
# It's recommended to replace :latest with a specific version
# for example: docker.io/copyparty/ac:1.19.15
Image=docker.io/copyparty/ac:latest
ContainerName=copyparty
# Uncomment to enable auto-updates
# AutoUpdate=registry
# Environment variables
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
Environment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
Environment=PYTHONUNBUFFERED=1
# Ports
PublishPort=3923:3923
# Volumes (PLEASE LOOK!)
# Rootful setup:
# Leave as-is
# Non-root setup:
# Change /etc/copyparty to /home/<USER>/copyparty/config
Volume=/etc/copyparty:/cfg:z
# Rootful setup:
# Change /mnt to the directory you want to share
# Non-root setup:
# Change /mnt to something owned by your user, e.g., /home/<USER>/copyparty/sharing:/w:z
Volume=/mnt:/w:z
# Give the container time to stop in case the thumbnailer is still running.
# It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer
StopTimeout=15
# hide it from logs with "/._" so it matches the default --lf-url filter
HealthCmd="wget --spider -q 127.0.0.1:3923/?reset=/._"
HealthInterval=1m
HealthTimeout=2s
HealthRetries=5
HealthStartPeriod=15s
[Unit]
After=default.target
[Install]
# Start by default on boot
WantedBy=default.target
[Service]
# Give the container time to start in case it needs to pull the image
TimeoutStartSec=600

71
contrib/setup-ashell.sh Normal file
View file

@ -0,0 +1,71 @@
#!/bin/bash
#
# this script will install copyparty onto an iOS device (iPhone/iPad)
#
# step 1: install a-Shell:
# https://apps.apple.com/us/app/a-shell/id1473805438
#
# step 2: copypaste the following command into a-Shell:
# curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh
#
# step 3: launch copyparty with this command: cpp
#
# if you ever want to upgrade copyparty, just repeat step 2
cd "$HOME/Documents"
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
# create the config file? (cannot use heredoc because body too large)
[ -e cpc ] || {
echo '[global]' >cpc
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
echo ' e2ts # and enable multimedia indexing' >>cpc
echo ' ver # show copyparty version in the controlpanel' >>cpc
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
echo ' qrp: 1 # reduce qr-code padding' >>cpc
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
echo '' >>cpc
echo ' # enable these by uncommenting them:' >>cpc
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
echo '' >>cpc
echo '[/]' >>cpc
echo ' ~/Documents' >>cpc
echo ' accs:' >>cpc
echo ' A: *' >>cpc
}
# create the launcher?
[ -e cpp ] || {
echo '#!/bin/sh' >cpp
echo '' >>cpp
echo '# change the font so the qr-code draws correctly:' >>cpp
echo 'config -n "Menlo" # name' >>cpp
echo 'config -s 8 # size' >>cpp
echo '' >>cpp
echo '# launch copyparty' >>cpp
echo 'exec copyparty -c cpc "$@"' >>cpp
}
chmod 755 copyparty cpp
echo
echo =================================
echo
echo 'okay, all done!'
echo
echo 'you can edit your config'
echo 'with this command: vim cpc'
echo
echo 'you can run copyparty'
echo 'with this command: cpp'
echo

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

View file

@ -1,5 +1,18 @@
# ./traefik --experimental.fastproxy=true --entrypoints.web.address=:8080 --providers.file.filename=copyparty.yaml
# ./traefik --configFile=copyparty.yaml
entryPoints:
web:
address: :8080
transport:
# don't disconnect during big uploads
respondingTimeouts:
readTimeout: "0s"
log:
level: DEBUG
providers:
file:
# WARNING: must be same filename as current file
filename: "copyparty.yaml"
http:
services:
service-cpp:

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

@ -55,7 +55,7 @@ except:
zs = """
web/a/partyfuse.py
web/a/u2c.py
web/a/webdav-cfg.bat
web/a/webdav-cfg.txt
web/baguettebox.js
web/browser.css
web/browser.html
@ -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
@ -91,6 +88,7 @@ web/mde.html
web/mde.js
web/msg.css
web/msg.html
web/opds.xml
web/rups.css
web/rups.html
web/rups.js
@ -102,19 +100,45 @@ web/splash.html
web/splash.js
web/svcs.html
web/svcs.js
web/tl/chi.js
web/tl/cze.js
web/tl/deu.js
web/tl/epo.js
web/tl/fin.js
web/tl/fra.js
web/tl/grc.js
web/tl/ita.js
web/tl/kor.js
web/tl/nld.js
web/tl/nno.js
web/tl/nor.js
web/tl/pol.js
web/tl/por.js
web/tl/rus.js
web/tl/spa.js
web/tl/swe.js
web/tl/tur.js
web/tl/ukr.js
web/ui.css
web/up2k.js
web/util.js
web/w.hash.js
"""
RES = set(zs.strip().split("\n"))
RESM = {
"web/a/partyfuse.txt": "web/a/partyfuse.py",
"web/a/u2c.txt": "web/a/u2c.py",
"web/a/webdav-cfg.bat": "web/a/webdav-cfg.txt",
}
class EnvParams(object):
def __init__(self) -> None:
self.t0 = time.time()
self.mod = ""
self.mod_ = ""
self.cfg = ""
self.scfg = True
E = EnvParams()

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 16, 10)
CODENAME = "COPYparty"
BUILD_DT = (2025, 1, 25)
VERSION = (1, 19, 21)
CODENAME = "usernames"
BUILD_DT = (2025, 12, 2)
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

@ -2,15 +2,22 @@
from __future__ import print_function, unicode_literals
import os
import time
from ..util import SYMTIME, fsdec, fsenc
from . import path as path
if True: # pylint: disable=using-constant-test
from typing import Any, Optional
from typing import Any, Optional, Union
_ = (path,)
__all__ = ["path"]
from ..util import NamedLogger
MKD_755 = {"chmod_d": 0o755}
MKD_700 = {"chmod_d": 0o700}
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
# 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 +27,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) or bname in todo:
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:
@ -76,6 +103,44 @@ def utime(
return os.utime(fsenc(p), times)
def utime_c(
log: Union["NamedLogger", Any],
p: str,
ts: int,
follow_symlinks: bool = True,
throw: bool = False,
) -> Optional[int]:
clamp = 0
ov = ts
bp = fsenc(p)
now = int(time.time())
while True:
try:
if SYMTIME:
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
else:
os.utime(bp, (now, ts))
if clamp:
t = "filesystem rejected utime(%r); clamped %s to %s"
log(t % (p, ov, ts))
return ts
except Exception as ex:
pv = ts
while clamp < len(UTIME_CLAMPS):
fun, cv = UTIME_CLAMPS[clamp]
ts = fun(ts, cv)
clamp += 1
if ts != pv:
break
if clamp >= len(UTIME_CLAMPS):
if throw:
raise
else:
t = "could not utime(%r) to %s; %s, %r"
log(t % (p, ov, ex, ex))
return None
if hasattr(os, "lstat"):
def lstat(p: str) -> os.stat_result:

View file

@ -12,6 +12,7 @@ import queue
from .__init__ import ANYWIN
from .authsrv import AuthSrv
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
from .fsutil import ramdisk_chk
from .httpsrv import HttpSrv
from .util import FAKE_MP, Daemon, HMaccas
@ -56,6 +57,7 @@ class MpWorker(BrokerCli):
# starting to look like a good idea
self.asrv = AuthSrv(args, None, False)
ramdisk_chk(self.asrv)
# instantiate all services here (TODO: inheritance?)
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
@ -99,6 +101,7 @@ class MpWorker(BrokerCli):
if dest == "reload":
self.logw("mpw.asrv reloading")
self.asrv.reload()
ramdisk_chk(self.asrv)
self.logw("mpw.asrv reloaded")
continue

View file

@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals
import argparse
import traceback
from queue import Queue

View file

@ -1,13 +1,12 @@
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")
@ -21,6 +20,19 @@ else:
VF = {"mv_re_t": 0, "rm_re_t": 0}
def _sp_err(exe, what, rc, so, se, sin):
try:
zs = shutil.which(exe)
except:
zs = "<?>"
try:
zi = os.path.getsize(zs)
except:
zi = 0
t = "failed to %s; error %s using %s (%s):\n STDOUT: %s\n STDERR: %s\n STDIN: %s\n"
raise Exception(t % (what, rc, zs, zi, so, se, sin.decode("utf-8")))
def ensure_cert(log: "RootLogger", args) -> None:
"""
the default cert (and the entire TLS support) is only here to enable the
@ -109,20 +121,20 @@ def _gen_ca(log: "RootLogger", args):
cmd = "cfssl gencert -initca -"
rc, so, se = runcmd(cmd.split(), 30, sin=sin)
if rc:
raise Exception("failed to create ca-cert: {}, {}".format(rc, se), 3)
_sp_err("cfssl", "create ca-cert", rc, so, se, sin)
cmd = "cfssljson -bare ca"
sin = so.encode("utf-8")
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
if rc:
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
_sp_err("cfssljson", "translate ca-cert", rc, so, se, sin)
bname = os.path.join(args.crt_dir, "ca")
try:
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)
@ -132,6 +144,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
names = args.crt_ns.split(",") if args.crt_ns else []
names = [x.strip() for x in names]
if not args.crt_exact:
for n in names[:]:
names.append("*.{}".format(n))
@ -202,20 +215,20 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"]
rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)
if rc:
raise Exception("failed to create cert: {}, {}".format(rc, se))
_sp_err("cfssl", "create cert", rc, so, se, sin)
cmd = "cfssljson -bare srv"
sin = so.encode("utf-8")
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
if rc:
raise Exception("failed to translate cert: {}, {}".format(rc, se))
_sp_err("cfssljson", "translate cert", rc, so, se, sin)
bname = os.path.join(args.crt_dir, "srv")
try:
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"""
@ -16,15 +19,18 @@ def vf_bmap() -> dict[str, str]:
"no_clone": "noclone",
"no_dirsz": "nodirsz",
"no_dupe": "nodupe",
"no_dupe_m": "nodupem",
"no_forget": "noforget",
"no_pipe": "nopipe",
"no_robots": "norobots",
"no_tail": "notail",
"no_thumb": "dthumb",
"no_vthumb": "dvthumb",
"no_athumb": "dathumb",
}
for k in (
"dedup",
"dlni",
"dotsrch",
"e2d",
"e2ds",
@ -40,17 +46,35 @@ def vf_bmap() -> dict[str, str]:
"gsel",
"hardlink",
"magic",
"md_no_br",
"no_db_ip",
"no_sb_md",
"no_sb_lg",
"nsort",
"og",
"og_no_head",
"og_s_title",
"opds",
"rand",
"reflink",
"rm_partial",
"rmagic",
"rss",
"ui_noacci",
"ui_nocpla",
"ui_nolbar",
"ui_nombar",
"ui_nonav",
"ui_notree",
"ui_norepl",
"ui_nosrvi",
"ui_noctxb",
"wo_up_readme",
"wram",
"xdev",
"xlink",
"xvol",
"zipmaxu",
):
ret[k] = k
return ret
@ -59,6 +83,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 +94,24 @@ def vf_vmap() -> dict[str, str]:
"th_x3": "th3x",
}
for k in (
"bup_ck",
"casechk",
"chmod_d",
"chmod_f",
"dbd",
"du_who",
"ufavico",
"forget_ip",
"hsortn",
"html_head",
"html_head_s",
"lg_sbf",
"md_sbf",
"lg_sba",
"md_sba",
"md_hist",
"nrand",
"u2ow",
"og_desc",
"og_site",
"og_th",
@ -86,14 +121,31 @@ def vf_vmap() -> dict[str, str]:
"og_title_i",
"og_tpl",
"og_ua",
"opds_exts",
"put_ck",
"put_name",
"mv_retry",
"rm_retry",
"shr_who",
"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
@ -105,6 +157,7 @@ def vf_cmap() -> dict[str, str]:
for k in (
"exp_lg",
"exp_md",
"ext_th",
"mte",
"mth",
"mtp",
@ -143,15 +196,27 @@ 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)",
"nodupem": "rejects existing files during moves as well",
"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",
"wram": "allow uploading into ramdisks",
"sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage",
"rm_partial": "delete unfinished uploads from HDD when they timeout",
"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": {
@ -160,8 +225,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",
@ -170,6 +237,7 @@ flagcats = {
"upload rotation\n(moves all uploads into the specified folder structure)": {
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
"rotf_tz=Europe/Oslo": "timezone (default=UTC)",
"lifetime=3600": "uploads are deleted after 1 hour",
},
"database, general": {
@ -178,19 +246,27 @@ 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",
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
"xlink": "cross-volume dupe detection / linking (dangerous)",
"xdev": "do not descend into other filesystems",
"xvol": "do not follow symlinks leaving the volume root",
@ -199,6 +275,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",
},
@ -211,7 +289,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",
@ -234,10 +315,20 @@ 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",
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
"unlist": "dont list files matching REGEX",
"dlni": "force-download (no-inline) files on click",
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
"html_head_s=TXT": "additional static text in the html <head>",
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
"nodirsz": "don't show total folder size",
"du_who=all": "show disk-usage info to everyone",
"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)",
@ -247,11 +338,67 @@ flagcats = {
"md_sba": "value of iframe allow-prop for markdown-sandbox",
"lg_sba": "value of iframe allow-prop for *logue-sandbox",
"nohtml": "return html and markdown as text/html",
"ui_noacci": "hide account-info in the UI",
"ui_nocpla": "hide cpanel-link in the UI",
"ui_nolbar": "hide link-bar in the UI",
"ui_nombar": "hide top-menu in the UI",
"ui_nonav": "hide navpane+breadcrumbs in the UI",
"ui_notree": "hide navpane in the UI",
"ui_norepl": "hide repl-button in the UI",
"ui_nosrvi": "hide server-info in the UI",
"ui_noctxb": "hide context-buttons in the UI",
},
"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",
},
"opds": {
"opds": "enable OPDS",
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
},
"textfiles": {
"md_no_br": "newline only on double-newline or two tailing spaces",
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
"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",
"shr_who=auth": "who can create shares? no/auth/a",
"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",
@ -261,3 +408,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

@ -7,7 +7,7 @@ import re
import time
from .__init__ import ANYWIN, MACOS
from .authsrv import AXS, VFS
from .authsrv import AXS, VFS, AuthSrv
from .bos import bos
from .util import chkcmd, min_ex, undot
@ -18,22 +18,25 @@ if True: # pylint: disable=using-constant-test
class Fstab(object):
def __init__(self, log: "RootLogger", args: argparse.Namespace):
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
self.log_func = log
self.verbose = verbose
self.warned = False
self.trusted = False
self.tab: Optional[VFS] = None
self.oldtab: Optional[VFS] = None
self.srctab = "a"
self.cache: dict[str, str] = {}
self.cache: dict[str, tuple[str, str]] = {}
self.age = 0.0
self.maxage = args.mtab_age
def log(self, msg: str, c: Union[int, str] = 0) -> None:
if not c or self.verbose:
return
self.log_func("fstab", msg, c)
def get(self, path: str) -> str:
def get(self, path: str) -> tuple[str, str]:
now = time.time()
if now - self.age > self.maxage or len(self.cache) > 9000:
self.age = now
@ -41,6 +44,7 @@ class Fstab(object):
self.tab = None
self.cache = {}
mp = ""
fs = "ext4"
msg = "failed to determine filesystem at %r; assuming %s\n%s"
@ -50,7 +54,7 @@ class Fstab(object):
path = self._winpath(path)
except:
self.log(msg % (path, fs, min_ex()), 3)
return fs
return fs, ""
path = undot(path)
try:
@ -59,14 +63,14 @@ class Fstab(object):
pass
try:
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path)
except:
self.log(msg % (path, fs, min_ex()), 3)
fs = fs.lower()
self.cache[path] = fs
self.log("found %s at %r" % (fs, path))
return fs
self.cache[path] = (fs, mp)
self.log("found %s at %r, %r" % (fs, mp, path))
return fs, mp
def _winpath(self, path: str) -> str:
# try to combine volume-label + st_dev (vsn)
@ -78,42 +82,58 @@ 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:
self.log("inspecting mtab for changes")
def _from_sp_mount(self) -> dict[str, str]:
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
if MACOS:
sptn = r"^.*? on (.*) \(([^ ]+), .*"
ptn = re.compile(sptn)
so, _ = chkcmd(["mount"])
tab1: list[tuple[str, str]] = []
atab = []
dtab: dict[str, str] = {}
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
zs1, zs2 = m.groups()
tab1.append((str(zs1), str(zs2)))
atab.append(ln)
dtab[str(zs1)] = str(zs2)
return dtab
def _from_proc(self) -> dict[str, str]:
ret: dict[str, str] = {}
with open("/proc/self/mounts", "rb", 262144) as f:
src = f.read(262144).decode("utf-8", "replace").split("\n")
for zsl in [x.split(" ") for x in src]:
if len(zsl) < 3:
continue
zs = zsl[1]
zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
ret[zs] = zsl[2]
return ret
def build_tab(self) -> None:
self.log("inspecting mtab for changes")
dtab = self._from_sp_mount() if MACOS else self._from_proc()
# keep empirically-correct values if mounttab unchanged
srctab = "\n".join(sorted(atab))
srctab = str(sorted(dtab.items()))
if srctab == self.srctab:
self.tab = self.oldtab
return
self.log("mtab has changed; reevaluating support for sparse files")
tab1 = list(dtab.items())
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 +150,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
@ -144,7 +165,7 @@ class Fstab(object):
vn.realpath = ptn.sub(nval, vn.realpath)
visit.extend(list(vn.nodes.values()))
def get_unix(self, path: str) -> str:
def get_unix(self, path: str) -> tuple[str, str]:
if not self.tab:
try:
self.build_tab()
@ -153,20 +174,50 @@ class Fstab(object):
# prisonparty or other restrictive environment
if not self.warned:
self.warned = True
self.log("failed to build tab:\n{}".format(min_ex()), 3)
t = "failed to associate fs-mounts with the VFS (this is fine):\n%s"
self.log(t % (min_ex(),), 6)
self.build_fallback()
assert self.tab # !rm
ret = self.tab._find(path)[0]
if self.trusted or path == ret.vpath:
return ret.realpath.split("/")[0]
return ret.realpath.split("/")[0], ret.vpath
else:
return "idk"
return "idk", ""
def get_w32(self, path: str) -> str:
def get_w32(self, path: str) -> tuple[str, str]:
if not self.tab:
self.build_fallback()
assert self.tab # !rm
ret = self.tab._find(path)[0]
return ret.realpath
return ret.realpath, ""
def ramdisk_chk(asrv: AuthSrv) -> None:
# should have been in authsrv but that's a circular import
mods = []
ramfs = ("tmpfs", "overlay")
log = asrv.log_func or print
fstab = Fstab(log, asrv.args, False)
for vn in asrv.vfs.all_nodes.values():
if not vn.axs.uwrite or "wram" in vn.flags:
continue
ap = vn.realpath
if not ap or os.path.isfile(ap):
continue
fs, mp = fstab.get(ap)
mp = "/" + mp.strip("/")
if fs == "tmpfs" or (mp == "/" and fs in ramfs):
mods.append((vn.vpath, ap, fs, mp))
vn.axs.uwrite.clear()
vn.axs.umove.clear()
for un, ztsp in list(vn.uaxs.items()):
zsl = list(ztsp)
zsl[1] = False
zsl[2] = False
vn.uaxs[un] = zsl
if mods:
t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
log("vfs", t + "".join(t2) + "\n", 1)

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,
)
@ -66,13 +68,13 @@ class FtpAuth(DummyAuthorizer):
if ip.startswith("::ffff:"):
ip = ip[7:]
ip = ipnorm(ip)
ipn = ipnorm(ip)
bans = self.hub.bans
if ip in bans:
rt = bans[ip] - time.time()
if ipn in bans:
rt = bans[ipn] - time.time()
if rt < 0:
logging.info("client unbanned")
del bans[ip]
del bans[ipn]
else:
raise AuthenticationFailed("banned")
@ -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
@ -141,10 +152,6 @@ class FtpFs(AbstractedFS):
self.cwd = "/" # pyftpdlib convention of leading slash
self.root = "/var/lib/empty"
self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False
self.can_admin = self.can_dot = False
self.listdirinfo = self.listdir
self.chdir(".")
@ -170,6 +177,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))
@ -181,10 +198,13 @@ class FtpFs(AbstractedFS):
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _, _, _ = avfs.can_access("", self.h.uname)
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise FSE("No such file or directory", 1)
return os.path.join(vfs.realpath, rem), vfs, rem
except Pebkac as ex:
raise FSE(str(ex))
@ -197,7 +217,7 @@ class FtpFs(AbstractedFS):
m: bool = False,
d: bool = False,
) -> tuple[str, VFS, str]:
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
return self.v2a(join(self.cwd, vpath), r, w, m, d)
def ftp2fs(self, ftppath: str) -> str:
# return self.v2a(ftppath)
@ -218,7 +238,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:
@ -230,8 +250,9 @@ class FtpFs(AbstractedFS):
td = 0
if w and need_unlink:
assert td # type: ignore # !rm
if td >= -1 and td <= self.args.ftp_wt:
# within permitted timeframe; unlink and accept
# within permitted timeframe; allow overwrite or resume
do_it = True
elif self.args.no_del or self.args.ftp_no_ow:
# file too old, or overwrite not allowed; reject
@ -248,13 +269,23 @@ class FtpFs(AbstractedFS):
if not do_it:
raise FSE("File already exists")
wunlink(self.log, ap, VF_CAREFUL)
# Don't unlink file for append mode
elif "a" not in mode:
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)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
if not vfs.realpath:
self.cwd = nwd
return
ap = vfs.canonical(rem)
try:
st = bos.stat(ap)
@ -269,20 +300,10 @@ class FtpFs(AbstractedFS):
raise FSE("Permission denied", 1)
self.cwd = nwd
(
self.can_read,
self.can_write,
self.can_move,
self.can_delete,
self.can_get,
self.can_upget,
self.can_admin,
self.can_dot,
) = 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)
@ -301,7 +322,7 @@ class FtpFs(AbstractedFS):
vfs_ls = [x[0] for x in vfs_ls1]
vfs_ls.extend(vfs_virt.keys())
if not self.can_dot:
if self.uname not in vfs.axs.udot:
vfs_ls = exclude_dotfiles(vfs_ls)
vfs_ls.sort()
@ -349,16 +370,13 @@ class FtpFs(AbstractedFS):
raise FSE(str(ex))
def rename(self, src: str, dst: str) -> None:
if not self.can_move:
raise FSE("Not allowed for user " + self.h.uname)
if self.args.no_mv:
raise FSE("The rename/move feature is disabled in server config")
svp = join(self.cwd, src).lstrip("/")
dvp = join(self.cwd, dst).lstrip("/")
try:
self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp)
except Exception as ex:
raise FSE(str(ex))
@ -382,7 +400,7 @@ class FtpFs(AbstractedFS):
def utime(self, path: str, timeval: float) -> None:
ap = self.rv2a(path, w=True)[0]
return bos.utime(ap, (timeval, timeval))
bos.utime_c(logging.warning, ap, int(timeval), False)
def lstat(self, path: str) -> os.stat_result:
ap = self.rv2a(path)[0]
@ -471,27 +489,37 @@ class FtpHandler(FTPHandler):
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
ap, vfs, rem = self.fs.v2a(vp, w=True)
try:
ap, vfs, rem = self.fs.v2a(vp, w=True)
except Exception as ex:
self.respond("550 %s" % (ex,), logging.info)
return
self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
None,
None,
self.hub.up2k,
"xbu.ftpd",
xbu,
ap,
vp,
"",
self.uname,
self.hub.asrv.vfs.get_perms(vp, self.uname),
0,
0,
self.cli_ip,
time.time(),
"",
):
raise FSE("Upload blocked by xbu server config")
if xbu:
hr = runhook(
None,
None,
self.hub.up2k,
"xbu.ftpd",
xbu,
ap,
vp,
"",
self.uname,
self.hub.asrv.vfs.get_perms(vp, self.uname),
0,
0,
self.cli_ip,
time.time(),
None,
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
if not t:
t = "Upload blocked by xbu server config: %r" % (vp,)
self.respond("550 %s" % (t,), logging.info)
return
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
ret = FTPHandler.ftp_STOR(self, file, mode)
@ -591,7 +619,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",
@ -184,6 +187,7 @@ class HttpSrv(object):
"svcs",
]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.j2["opds"] = env.get_template("opds.xml")
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
if self.args.ipu:
@ -191,6 +195,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 +322,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 +331,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)
@ -371,8 +383,8 @@ class HttpSrv(object):
if nloris < nconn / 2:
continue
t = "slowloris (idle-conn): {} banned for {} min"
self.log(self.name, t.format(ip, self.args.loris, nclose), 1)
t = "slow%s (idle-conn): %s banned for %d min" # slowloris
self.log(self.name, t % ("loris", ip, self.args.loris), 1)
self.bans[ip] = int(time.time() + self.args.loris * 60)
if self.args.log_conn:
@ -394,6 +406,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(
@ -547,7 +572,7 @@ class HttpSrv(object):
v = self.E.t0
try:
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
with os.scandir(self.E.mod_ + "web") as dh:
for fh in dh:
inf = fh.stat()
v = max(v, inf.st_mtime)

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

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import errno
import os
import random
import select
import socket
@ -12,22 +13,52 @@ from ipaddress import IPv4Network, IPv6Network
from .__init__ import TYPE_CHECKING
from .__init__ import unicode as U
from .multicast import MC_Sck, MCast
from .stolen.dnslib import AAAA
from .stolen.dnslib import CLASS as DC
from .stolen.dnslib import (
NSEC,
PTR,
QTYPE,
RR,
SRV,
TXT,
A,
DNSHeader,
DNSQuestion,
DNSRecord,
set_avahi_379,
)
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
try:
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_DNSLIB"):
raise ImportError()
from .stolen.dnslib import (
AAAA,
)
from .stolen.dnslib import CLASS as DC
from .stolen.dnslib import (
NSEC,
PTR,
QTYPE,
RR,
SRV,
TXT,
A,
DNSHeader,
DNSQuestion,
DNSRecord,
set_avahi_379,
)
DNS_VND = True
except ImportError:
DNS_VND = False
from dnslib import (
AAAA,
)
from dnslib import CLASS as DC
from dnslib import (
NSEC,
PTR,
QTYPE,
RR,
SRV,
TXT,
A,
Bimap,
DNSHeader,
DNSQuestion,
DNSRecord,
)
DC.forward[0x8001] = "F_IN"
DC.reverse["F_IN"] = 0x8001
if TYPE_CHECKING:
from .svchub import SvcHub
@ -35,6 +66,11 @@ if TYPE_CHECKING:
if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union
if os.getenv("PRTY_MODSPEC"):
from inspect import getsourcefile
print("PRTY_MODSPEC: dnslib:", getsourcefile(A))
MDNS4 = "224.0.0.251"
MDNS6 = "ff02::fb"
@ -73,10 +109,11 @@ class MDNS(MCast):
self.ngen = ngen
self.ttl = 300
if not self.args.zm_nwa_1:
if not self.args.zm_nwa_1 and DNS_VND:
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))
@ -99,9 +136,14 @@ class MDNS(MCast):
self.log_func(self.logsrc, msg, c)
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
ar = self.args
zms = self.args.zms
http = {"port": 80 if 80 in self.args.p else self.args.p[0]}
https = {"port": 443 if 443 in self.args.p else self.args.p[0]}
zi = ar.zm_http
http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}
zi = ar.zm_https
https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}
webdav = http.copy()
webdavs = https.copy()
webdav["u"] = webdavs["u"] = "u" # KDE requires username
@ -126,16 +168,16 @@ class MDNS(MCast):
svcs: dict[str, dict[str, Any]] = {}
if "d" in zms:
if "d" in zms and http["port"]:
svcs["_webdav._tcp.local."] = webdav
if "D" in zms:
if "D" in zms and https["port"]:
svcs["_webdavs._tcp.local."] = webdavs
if "h" in zms:
if "h" in zms and http["port"]:
svcs["_http._tcp.local."] = http
if "H" in zms:
if "H" in zms and https["port"]:
svcs["_https._tcp.local."] = https
if "f" in zms.lower():
@ -374,7 +416,7 @@ class MDNS(MCast):
cip = addr[0]
v6 = ":" in cip
if (cip.startswith("169.254") and not self.ll_ok) or (
v6 and not cip.startswith("fe80")
v6 and not cip.startswith(IP6_LL)
):
return

View file

@ -17,10 +17,10 @@ class Metrics(object):
self.hsrv = hsrv
def tx(self, cli: "HttpCli") -> bool:
if not cli.avol:
raise Pebkac(403, "not allowed for user " + cli.uname)
args = cli.args
if not cli.avol and cli.uname.lower() not in args.stats_u_set:
raise Pebkac(403, "'stats' not allowed for user " + cli.uname)
if not args.stats:
raise Pebkac(403, "the stats feature is not enabled in server config")

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,17 @@ 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))
if znil:
t += ", using " + znil[0][1].filename
log(t)
if not znil:
raise Exception("no images inside cbz")
fi = zf.open(znil[0][1])
using = sorted(znil)[0][1].filename
if znil:
t += ", using " + using
log(t)
fi = zf.open(using)
elif pk == "epub":
fi = get_cover_from_epub(log, abspath)
assert fi # !rm
else:
raise Exception("unknown compression %s" % (pk,))
@ -194,16 +200,17 @@ def au_unpk(
except Exception as ex:
if ret:
t = "failed to decompress audio file %r: %r"
t = "failed to decompress file %r: %r"
log(t % (abspath, ex))
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
return ""
return abspath
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 +224,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 +258,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 +286,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 +327,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 +376,83 @@ 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)
if not coverimage_path:
raise Exception("no cover inside epub")
# This url is either absolute (in the .epub) or relative to the package document
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
try:
return z.open(adjusted_cover_path)
except KeyError:
t = "epub: cover specified in package document, but doesn't exist: %s"
log(t % (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>
xn = package_root.find("./metadata/meta[@name='cover']", package_ns)
cover_id = xn.get("content") if xn is not None else None
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):
@ -424,7 +518,6 @@ class MTag(object):
"album-artist",
"tpe2",
"aart",
"conductor",
"organization",
"band",
],
@ -558,6 +651,9 @@ class MTag(object):
return self._get(abspath)
ap = au_unpk(self.log, self.args.au_unpk, abspath)
if not ap:
return {}
ret = self._get(ap)
if ap != abspath:
wunlink(self.log, ap, VF_CAREFUL)
@ -629,7 +725,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)):
@ -663,6 +759,9 @@ class MTag(object):
ap = abspath
ret: dict[str, Any] = {}
if not ap:
return ret
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
try:
cmd = [parser.bin, ap]

View file

@ -15,7 +15,7 @@ from ipaddress import (
)
from .__init__ import MACOS, TYPE_CHECKING
from .util import Daemon, Netdev, find_prefix, min_ex, spack
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
if TYPE_CHECKING:
from .svchub import SvcHub
@ -96,7 +96,10 @@ class MCast(object):
def create_servers(self) -> list[str]:
bound: list[str] = []
netdevs = self.hub.tcpsrv.netdevs
ips = [x[0] for x in self.hub.tcpsrv.bound]
blist = self.hub.tcpsrv.bound
if self.args.http_no_tcp:
blist = self.hub.tcpsrv.seen_eps
ips = [x[0] for x in blist]
if "::" in ips:
ips = [x for x in ips if x != "::"] + list(
@ -145,7 +148,7 @@ class MCast(object):
all_selected = ips[:]
# discard non-linklocal ipv6
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
if not ips:
raise NoIPs()
@ -163,6 +166,7 @@ class MCast(object):
sck.settimeout(None)
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
# safe for this purpose; https://lwn.net/Articles/853637/
sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except:
pass
@ -182,11 +186,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(IP64_LL)}
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
@ -25,6 +25,7 @@ class PWHash(object):
self.args = args
zsl = args.ah_alg.split(",")
zsl = [x.strip() for x in zsl]
alg = zsl[0]
if alg == "none":
alg = ""
@ -147,6 +148,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> ")

112
copyparty/qrkode.py Normal file
View file

@ -0,0 +1,112 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import os
try:
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_QRCG"):
raise ImportError()
from .stolen.qrcodegen import QrCode
qrgen = QrCode.encode_binary
VENDORED = True
except ImportError:
VENDORED = False
from qrcodegen import QrCode
if os.getenv("PRTY_MODSPEC"):
from inspect import getsourcefile
print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode))
if True: # pylint: disable=using-constant-test
import typing
from typing import Any, Optional, Sequence, Union
if not VENDORED:
def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode":
ret = None
V = QrCode.Ecc
for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:
qr = QrCode.encode_binary(data, e)
qr.size = qr._size
qr.modules = qr._modules
if not ret or ret.size > qr.size:
ret = qr
return ret
qrgen = _qrgen
def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:
tab = qr.modules
sz = qr.size
if sz % 2 and zoom == 1:
tab.append([False] * sz)
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
tab = [[False] * pad + x + [False] * pad for x in tab]
rows: list[str] = []
if zoom == 1:
for y in range(0, len(tab), 2):
row = ""
for x in range(len(tab[y])):
v = 2 if tab[y][x] else 0
v += 1 if tab[y + 1][x] else 0
row += " ▄▀█"[v]
rows.append(row)
else:
for tr in tab:
row = ""
for zb in tr:
row += ""[int(zb)] * 2
rows.append(row)
return "\n".join(rows)
def qr2png(
qr: QrCode,
zoom: int,
pad: int,
bg: Optional[tuple[int, int, int]],
fg: Optional[tuple[int, int, int]],
ap: str,
) -> None:
from PIL import Image
tab = qr.modules
sz = qr.size
psz = sz + pad * 2
if bg:
img = Image.new("RGB", (psz, psz), bg)
else:
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
fg = (fg[0], fg[1], fg[2], 255)
for y in range(sz):
for x in range(sz):
if tab[y][x]:
img.putpixel((x + pad, y + pad), fg)
if zoom != 1:
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
img.save(ap)
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

View file

@ -246,24 +246,29 @@ class SMB(object):
ap = absreal(ap)
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.smb",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"1.7.6.2",
time.time(),
"",
):
yeet("blocked by xbu server config: %r" % (vpath,))
if xbu:
hr = runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.smb",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"1.7.6.2",
time.time(),
None,
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
if not t:
t = "blocked by xbu server config: %r" % (vpath,)
yeet(t)
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
if wr:
@ -318,9 +323,9 @@ class SMB(object):
t = "blocked rename (no-move-acc %s): /%s @%s"
yeet(t % (vfs1.axs.umove, vp1, uname))
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2)
try:
bos.makedirs(ap2)
bos.makedirs(ap2, vf=vfs2.flags)
except:
pass
@ -334,7 +339,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:
@ -373,7 +378,7 @@ class SMB(object):
t = "blocked utime (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.utime(ap, times)
bos.utime_c(info, ap, int(times[1]), False)
def _p_exists(self, vpath: str) -> bool:
# ap = "?"

View file

@ -4,7 +4,7 @@
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
# the original ^ is extremely well commented so refer to that for explanations
# hacks: binary-only, auto-ecc, render, py2-compat
# hacks: binary-only, auto-ecc, py2-compat
from __future__ import print_function, unicode_literals
@ -173,33 +173,6 @@ class QrCode(object):
self._apply_mask(msk) # Apply the final choice of mask
self._draw_format_bits(msk) # Overwrite old format bits
def render(self, zoom=1, pad=4) -> str:
tab = self.modules
sz = self.size
if sz % 2 and zoom == 1:
tab.append([False] * sz)
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
tab = [[False] * pad + x + [False] * pad for x in tab]
rows: list[str] = []
if zoom == 1:
for y in range(0, len(tab), 2):
row = ""
for x in range(len(tab[y])):
v = 2 if tab[y][x] else 0
v += 1 if tab[y + 1][x] else 0
row += " ▄▀█"[v]
rows.append(row)
else:
for tr in tab:
row = ""
for zb in tr:
row += ""[int(zb)] * 2
rows.append(row)
return "\n".join(rows)
def _draw_function_patterns(self) -> None:
# Draw horizontal and vertical timing patterns
for i in range(self.size):
@ -594,20 +567,3 @@ def _get_bit(x: int, i: int) -> bool:
class DataTooLongError(ValueError):
pass
def qr2svg(qr: QrCode, border: int) -> str:
parts: list[str] = []
for y in range(qr.size):
sy = border + y
for x in range(qr.size):
if qr.modules[y][x]:
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
t = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
<rect width="100%" height="100%" fill="#F7F7F7"/>
<path d="{1}" fill="#111111"/>
</svg>
"""
return t.format(qr.size + border * 2, " ".join(parts))

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
@ -27,8 +27,10 @@ if True: # pylint: disable=using-constant-test
from typing import Any, Optional, Union
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who
from .bos import bos
from .cert import ensure_cert
from .fsutil import ramdisk_chk
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2
from .tcpsrv import TcpSrv
@ -38,6 +40,7 @@ from .th_srv import (
HAVE_FFPROBE,
HAVE_HEIF,
HAVE_PIL,
HAVE_RAW,
HAVE_VIPS,
HAVE_WEBP,
ThumbSrv,
@ -51,6 +54,7 @@ from .util import (
HAVE_PSUTIL,
HAVE_SQLITE3,
HAVE_ZMQ,
RE_ANSI,
URL_BUG,
UTC,
VERSIONS,
@ -60,19 +64,26 @@ from .util import (
HMaccas,
ODict,
alltrace,
ansi_re,
build_netmap,
expat_ver,
gzip,
html_escape,
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 +95,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.
@ -120,6 +136,7 @@ class SvcHub(object):
self.nsigs = 3
self.retcode = 0
self.httpsrv_up = 0
self.qr_tsz = None
self.log_mutex = threading.Lock()
self.cday = 0
@ -141,7 +158,8 @@ class SvcHub(object):
args.unpost = 0
args.no_del = True
args.no_mv = True
args.hardlink = True
args.reflink = True
args.dav_auth = True
args.vague_403 = True
args.nih = True
@ -158,6 +176,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 +205,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,8 +248,8 @@ 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:
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
if args.chpw and args.have_idp_hdrs and "pw" not in args.auth_ord.split(","):
t = "ERROR: user-changeable passwords is not compatible with your current configuration. Choose one of these options to fix it:\n option1: disable --chpw\n option2: remove all use of IdP features; --idp-*\n option3: change --auth-ord to something like pw,idp,ipu"
self.log("root", t, 1)
raise Exception(t)
@ -239,8 +264,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:
@ -250,6 +291,17 @@ class SvcHub(object):
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
args.theme = "{0}{1} {0} {1}".format(ch, bri)
if args.no_stack:
args.stack_who = "no"
if args.nid:
args.du_who = "no"
args.du_iwho = n_du_who(args.du_who)
if args.ver and args.ver_who == "no":
args.ver_who = "all"
args.ver_iwho = n_ver_who(args.ver_who)
if args.nih:
args.vname = ""
args.doctitle = args.doctitle.replace(" @ --name", "")
@ -263,6 +315,7 @@ class SvcHub(object):
# initiate all services to manage
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
ramdisk_chk(self.asrv)
if args.cgen:
self.asrv.cgen()
@ -287,11 +340,13 @@ class SvcHub(object):
self._feature_test()
decs = {k: 1 for k in self.args.th_dec.split(",")}
decs = {k.strip(): 1 for k in self.args.th_dec.split(",")}
if not HAVE_VIPS:
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)
@ -340,7 +395,10 @@ class SvcHub(object):
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
raise Exception(t % (args.q_mp3,))
else:
args.au_unpk = {}
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
args.au_unpk = {
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
}
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
@ -393,39 +451,100 @@ class SvcHub(object):
# create netmaps early to avoid firewall gaps,
# but the mutex blocks multiprocessing startup
for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split():
try:
getattr(args, zs).mutex = threading.Lock()
except:
pass
if args.ipr:
for nm in args.ipr_u.values():
nm.mutex = threading.Lock()
def _db_onfail_ses(self) -> None:
self.args.no_ses = True
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)
def setup_session_db(self) -> None:
if not HAVE_SQLITE3:
self.args.no_ses = True
t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
self.log("root", t, 3)
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
create = True
db_path = self.args.ses_db
self.log("root", "opening sessions-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 %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
if sver:
raise
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 n:
if tries or sver > native_ver:
raise
t = "sessions-db corrupt; deleting and recreating: %r"
self.log("root", t % (ex,), 3)
t = "%s is unusable; deleting and recreating: %r"
self.log("root", t % (desc, ex), 3)
try:
cur.close() # type: ignore
except:
@ -434,8 +553,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 +569,74 @@ class SvcHub(object):
r"create index us_t0 on us(t0)",
r"insert into kv values ('sver', 1)",
]
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new sessions-db")
return 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()
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 +645,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 +656,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):
try:
db = sqlite3.connect(db_path)
cur = db.cursor()
try:
cur.execute("select count(*) from sh").fetchone()
create = False
break
except:
pass
except Exception as ex:
if n:
raise
t = "shares-db corrupt; deleting and recreating: %r"
self.log("root", t % (ex,), 3)
try:
cur.close() # type: ignore
except:
pass
try:
db.close() # type: ignore
except:
pass
os.unlink(db_path)
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()
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()
except Exception as ex:
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
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 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 +709,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 +804,84 @@ class SvcHub(object):
def sigterm(self) -> None:
self.signal_handler(signal.SIGTERM, None)
def sticky_qr(self) -> None:
self._sticky_qr()
def _unsticky_qr(self, flush=True) -> None:
print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
if flush:
sys.stderr.flush()
def _sticky_qr(self, force: bool = False) -> None:
sz = termsize()
if self.qr_tsz == sz:
if not force:
return
else:
force = False
if self.qr_tsz:
self._unsticky_qr(False)
else:
atexit.register(self._unsticky_qr)
tw, th = self.qr_tsz = sz
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
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)
if not force:
self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
self.pr(t, file=sys.stderr, end="")
def _qr_thr(self):
qr = self.tcpsrv.qr
w8 = self.args.qr_wait
if w8:
time.sleep(w8)
self.log("qr-code", qr)
if self.args.qr_stdout:
self.pr(self.tcpsrv.qr)
if self.args.qr_stderr:
self.pr(self.tcpsrv.qr, file=sys.stderr)
w8 = self.args.qr_every
msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
while w8:
time.sleep(w8)
if self.stopping:
break
if self.args.qr_pin:
self._sticky_qr(True)
else:
self.log("qr-code", msg)
w8 = self.args.qr_winch
while w8:
time.sleep(w8)
if self.stopping:
break
self._sticky_qr()
def cb_httpsrv_up(self) -> None:
self.httpsrv_up += 1
if self.httpsrv_up != self.broker.num_workers:
@ -618,7 +894,17 @@ class SvcHub(object):
break
if self.tcpsrv.qr:
self.log("qr-code", self.tcpsrv.qr)
if self.args.qr_pin:
self.sticky_qr()
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
Daemon(self._qr_thr, "qr")
else:
if not self.args.qr_pin:
self.log("qr-code", self.tcpsrv.qr)
if self.args.qr_stdout:
self.pr(self.tcpsrv.qr)
if self.args.qr_stderr:
self.pr(self.tcpsrv.qr, file=sys.stderr)
else:
self.log("root", "workers OK\n")
@ -645,6 +931,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 +966,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 +978,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
@ -708,6 +1002,24 @@ class SvcHub(object):
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
if not al.no_ses:
al.no_ses = True
t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
if al.idp_store == 1:
al.idp_store = 0
self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
if al.idp_store:
t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
self.log("root", t2, 1)
raise Exception(t2)
if al.shr:
t2 = "ERROR: Cannot enable shares %s" % (t,)
self.log("root", t2, 1)
raise Exception(t2)
def _process_config(self) -> bool:
al = self.args
@ -763,13 +1075,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 stats_u".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 +1108,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,12 +1169,21 @@ 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])
if self.args.name_url:
zs = html_escape(self.args.name_url, True, True)
zs = '<a href="%s">%s</a>' % (zs, self.args.name)
else:
zs = self.args.name
self.args.name_html = zs
zs = al.u2sz
zsl = zs.split(",")
zsl = [x.strip() for x in zs.split(",")]
if len(zsl) not in (1, 3):
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
raise Exception(t)
@ -859,6 +1200,7 @@ class SvcHub(object):
zi2 = zi
al.u2sz = ",".join(zsl)
derive_args(al)
return True
def _ipa2re(self, txt) -> Optional[re.Pattern]:
@ -931,7 +1273,7 @@ class SvcHub(object):
fn = sel_fn
try:
os.makedirs(os.path.dirname(fn))
bos.makedirs(os.path.dirname(fn))
except:
pass
@ -948,6 +1290,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]
@ -1031,6 +1376,7 @@ class SvcHub(object):
with self.reload_mutex:
self.log("root", "reloading config")
self.asrv.reload(9 if up2k else 4)
ramdisk_chk(self.asrv)
if up2k:
self.up2k.reload(rescan_all_vols)
t += "; volumes are now reinitializing"
@ -1215,11 +1561,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 +1613,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 +1633,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

@ -9,24 +9,26 @@ import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .cert import gencert
from .stolen.qrcodegen import QrCode
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
from .util import (
E_ACCESS,
E_ADDR_IN_USE,
E_ADDR_NOT_AVAIL,
E_UNREACH,
HAVE_IPV6,
IP6_LL,
IP6ALL,
VF_CAREFUL,
Netdev,
atomic_move,
get_adapters,
min_ex,
sunpack,
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
@ -58,6 +60,7 @@ class TcpSrv(object):
self.stopping = False
self.srv: list[socket.socket] = []
self.bound: list[tuple[str, int]] = []
self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only
self.netdevs: dict[str, Netdev] = {}
self.netlist = ""
self.nsrv = 0
@ -140,25 +143,31 @@ class TcpSrv(object):
# keep IPv6 LL-only nics
ll_ok: set[str] = set()
for ip, nd in self.netdevs.items():
if not ip.startswith("fe80"):
if not ip.startswith(IP6_LL):
continue
just_ll = True
for ip2, nd2 in self.netdevs.items():
if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
just_ll = False
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)"
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
if ip.startswith("fe80") and ip not in ll_ok:
if ip.startswith(IP6_LL) and ip not in ll_ok:
continue
for port in sorted(self.args.p):
@ -169,6 +178,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 +232,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 +248,10 @@ class TcpSrv(object):
def _listen(self, ip: str, port: int) -> None:
uds_perm = uds_gid = -1
bound: Optional[socket.socket] = None
tcp = False
if "unix:" in ip:
tcp = False
ipv = socket.AF_UNIX
uds = ip.split(":")
ip = uds[-1]
@ -241,7 +264,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 +277,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,17 +292,28 @@ 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:
if self.args.http_no_tcp:
self.seen_eps.append((ip, port))
return
srv.bind((ip, port))
else:
if ANYWIN or self.args.rm_sck:
if os.path.exists(ip):
os.unlink(ip)
srv.bind(ip)
if uds_gid != -1:
os.chown(ip, -1, uds_gid)
if uds_perm != -1:
os.chmod(ip, uds_perm)
else:
tf = "%s.%d" % (ip, os.getpid())
if os.path.exists(tf):
@ -375,6 +414,7 @@ class TcpSrv(object):
self.srv = srvs
self.bound = bound
self.seen_eps = list(set(self.seen_eps + bound))
self.nsrv = len(srvs)
self._distribute_netdevs()
@ -417,9 +457,7 @@ class TcpSrv(object):
self._distribute_netdevs()
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 +586,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 +603,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 +615,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,9 +623,17 @@ 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)
qrc = qrgen(btxt)
for zs in self.args.qr_file or []:
self._qr2file(qrc, zs)
if zoom == 0:
try:
tw, th = termsize()
@ -597,13 +642,15 @@ class TcpSrv(object):
except:
zoom = 1
qr = qrc.render(zoom, pad)
qr = qr2txt(qrc, zoom, pad)
if self.args.no_ansi:
return "{}\n{}".format(txt, qr)
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
if not fg:
halfc = "\033[0;40m{1}\033[0;47m"
if nocolor:
halfc = "\033[0;7m{1}\033[0m"
def ansify(m: re.Match) -> str:
return halfc.format(fg, " " * len(m.group(1)), bg)
@ -613,6 +660,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:
@ -620,3 +669,29 @@ class TcpSrv(object):
t = t.replace("\n", "`\n`")
return txt + t
def _qr2file(self, qrc: QrCode, txt: str):
if ".txt:" in txt or ".svg:" in txt:
ap, zs1, zs2 = txt.rsplit(":", 2)
bg = fg = ""
else:
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
zoom = int(zs1)
pad = int(zs2)
if ap.endswith(".txt"):
if zoom not in (1, 2):
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
with open(ap, "wb") as f:
f.write(qr2txt(qrc, zoom, pad).encode("utf-8"))
elif ap.endswith(".svg"):
with open(ap, "wb") as f:
f.write(qr2svg(qrc, pad).encode("utf-8"))
else:
qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)
def _h2i(self, hs):
try:
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
except:
return None

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,))
@ -340,24 +363,29 @@ class Tftpd(object):
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.tftpd",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"8.3.8.7",
time.time(),
"",
):
yeet("blocked by xbu server config: %r" % (vpath,))
if xbu:
hr = runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.tftpd",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"8.3.8.7",
time.time(),
None,
)
t = hr.get("rejectmsg") or ""
if t or hr.get("rc") != 0:
if not t:
t = "upload blocked by xbu server config: %r" % (vpath,)
yeet(t)
if not self.args.tftp_nols and bos.path.isdir(ap):
return self._ls(vpath, "", 0, True)
@ -365,18 +393,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 +434,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 +442,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)):
return None
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,7 +86,10 @@ try:
if os.environ.get("PRTY_NO_PIL_HEIF"):
raise Exception()
from pyheif_pillow_opener import register_heif_opener
try:
from pillow_heif import register_heif_opener
except ImportError:
from pyheif_pillow_opener import register_heif_opener
register_heif_opener()
HAVE_HEIF = True
@ -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
@ -227,6 +270,9 @@ class ThumbSrv(object):
def shutdown(self) -> None:
self.stopping = True
Daemon(self._fire_sentinels, "thumbstopper")
def _fire_sentinels(self):
for _ in range(self.nthr):
self.q.put(None)
@ -255,7 +301,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 +317,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 +345,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,
@ -336,12 +384,14 @@ class ThumbSrv(object):
else:
ap_unpk = abspath
if not bos.path.exists(tpath):
if ap_unpk and not bos.path.exists(tpath):
tex = tpath.rsplit(".", 1)[-1]
want_mp3 = tex == "mp3"
want_opus = tex in ("opus", "owa", "caf")
want_flac = tex == "flac"
want_wav = tex == "wav"
want_png = tex == "png"
want_au = want_mp3 or want_opus
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 +401,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
@ -371,12 +427,14 @@ class ThumbSrv(object):
except:
pass
conv_ok = False
for fun in funs:
try:
if not png_ok and tpath.endswith(".png"):
raise Exception("png only allowed for waveforms")
fun(ap_unpk, ttpath, fmt, vn)
conv_ok = True
break
except Exception as ex:
msg = "%s could not create thumbnail of %r\n%s"
@ -385,8 +443,12 @@ class ThumbSrv(object):
self.log(msg, c)
if getattr(ex, "returncode", 0) != 321:
if fun == funs[-1]:
with open(ttpath, "wb") as _:
pass
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:
@ -394,18 +456,33 @@ class ThumbSrv(object):
except:
pass
if abspath != ap_unpk:
if abspath != ap_unpk and ap_unpk:
wunlink(self.log, ap_unpk, vn.flags)
try:
wrename(self.log, ttpath, tpath, vn.flags)
except:
pass
atomic_move(self.log, ttpath, tpath, vn.flags)
except Exception as ex:
if conv_ok and not os.path.exists(tpath):
t = "failed to move [%s] to [%s]: %r"
self.log(t % (ttpath, tpath, ex), 3)
elif not conv_ok:
try:
open(tpath, "ab").close()
except:
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,35 +521,38 @@ class ThumbSrv(object):
return im
def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None:
try:
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.getres(vn, fmt))
fmts = ["RGB", "L"]
args = {"quality": 40}
if tpath.endswith(".webp"):
# quality 80 = pillow-default
# quality 75 = ffmpeg-default
# method 0 = pillow-default, fast
# method 4 = ffmpeg-default
# method 6 = max, slow
fmts.extend(("RGBA", "LA"))
args["method"] = 6
else:
# default q = 75
args["progressive"] = True
if im.mode not in fmts:
# print("conv {}".format(im.mode))
im = im.convert("RGB")
im.save(tpath, **args)
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
with Image.open(fsenc(abspath)) as im:
try:
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.getres(vn, fmt))
fmts = ["RGB", "L"]
args = {"quality": 40}
if tpath.endswith(".webp"):
# quality 80 = pillow-default
# quality 75 = ffmpeg-default
# method 0 = pillow-default, fast
# method 4 = ffmpeg-default
# method 6 = max, slow
fmts.extend(("RGBA", "LA"))
args["method"] = 6
else:
# default q = 75
args["progressive"] = True
if im.mode not in fmts:
# print("conv {}".format(im.mode))
im = im.convert("RGB")
im.save(tpath, **args)
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)
@ -495,9 +575,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 +632,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 +661,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,16 +682,16 @@ 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
c: Union[str, int] = "90"
t = "FFmpeg failed (probably a corrupt video file):\n"
t = "FFmpeg failed (probably a corrupt file):\n"
if (
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
and cmd[-1].lower().endswith(b".webp")
@ -595,7 +730,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 +768,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 +787,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 +870,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 +891,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 +899,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 +930,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 +1046,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 +1086,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 +1113,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 +1132,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 +1234,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

@ -53,6 +53,11 @@ class U2idx(object):
self.log("your python does not have sqlite3; searching will be disabled")
return
if self.args.srch_icase:
self._open_db = self._open_db_icase
else:
self._open_db = self._open_db_std
assert sqlite3 # type: ignore # !rm
self.active_id = ""
@ -69,6 +74,16 @@ class U2idx(object):
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("u2idx", msg, c)
def _open_db_std(self, *args, **kwargs):
assert sqlite3 # type: ignore # !rm
kwargs["check_same_thread"] = False
return sqlite3.connect(*args, **kwargs)
def _open_db_icase(self, *args, **kwargs):
db = self._open_db_std(*args, **kwargs)
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
return db
def shutdown(self) -> None:
if not HAVE_SQLITE3:
return
@ -134,9 +149,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")
@ -148,8 +163,7 @@ class U2idx(object):
uri = ""
try:
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
cur = db.cursor()
cur = self._open_db(uri, timeout=2, uri=True).cursor()
cur.execute('pragma table_info("up")').fetchone()
self.log("ro: %r" % (db_path,))
except:
@ -160,7 +174,7 @@ class U2idx(object):
if not cur:
# on windows, this steals the write-lock from up2k.deferred_init --
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
cur = self._open_db(db_path, timeout=2).cursor()
self.log("opened %r" % (db_path,))
self.cur[ptop] = cur
@ -173,6 +187,8 @@ class U2idx(object):
if not HAVE_SQLITE3:
return [], [], False
icase = self.args.srch_icase
q = ""
v: Union[str, int] = ""
va: list[Union[str, int]] = []
@ -180,6 +196,7 @@ class U2idx(object):
is_key = True
is_size = False
is_date = False
is_wark = False
field_end = "" # closing parenthesis or whatever
kw_key = ["(", ")", "and ", "or ", "not "]
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
@ -198,6 +215,8 @@ class U2idx(object):
is_key = kw in kw_key
uq = uq[len(kw) :]
ok = True
if is_wark:
kw = "= "
q += kw
break
@ -232,9 +251,17 @@ class U2idx(object):
elif v == "path":
v = "trim(?||up.rd,'/')"
va.append("\nrd")
if icase:
v = "casefold(%s)" % (v,)
elif v == "name":
v = "up.fn"
if icase:
v = "casefold(%s)" % (v,)
elif v == "w":
v = "substr(up.w,1,16)"
is_wark = True
elif v == "tags" or ptn_mt.match(v):
have_mt = True
@ -247,7 +274,7 @@ class U2idx(object):
v = "exists(select 1 from mt where mt.w = mtw and " + vq
else:
raise Pebkac(400, "invalid key [{}]".format(v))
raise Pebkac(400, "invalid key %r" % (v,))
q += v + " "
continue
@ -276,6 +303,14 @@ class U2idx(object):
is_size = False
v = int(float(v) * 1024 * 1024)
elif is_wark:
is_wark = False
v = v.strip("*")
if len(v) > 16:
v = v[:16]
if len(v) < 16:
raise Pebkac(400, "w/filehash must be 16+ chars")
else:
if v.startswith("*"):
head = "'%'||"
@ -285,6 +320,12 @@ class U2idx(object):
tail = "||'%'"
v = v[:-1]
if icase and "casefold(" in q:
try:
v = unicode(v).casefold()
except:
v = unicode(v).lower()
q += " {}?{} ".format(head, tail)
va.append(v)
is_key = True
@ -319,7 +360,7 @@ class U2idx(object):
uname: str,
vols: list[VFS],
uq: str,
uv: list[Union[str, int]],
uv: Union[list[str], list[Union[str, int]]],
have_mt: bool,
sort: bool,
lim: int,
@ -391,7 +432,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)

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