Compare commits

...
900 Commits
Author SHA1 Message Date
ed c41ee3fc27 v1.20.3 2026-01-21 05:05:50 +00:00
ed 6dcb1efb7c add ?smsg 2026-01-21 04:55:50 +00:00
ed 0a3a80725a fix jumpvol ?ls v2
the missing part of 66750391ae
2026-01-21 03:24:00 +00:00
ed ba67b27946 no racing 2026-01-21 03:19:41 +00:00
ed b4df8fa23c multipart-parser correctness (closes #1227);
makes it possible to login from the webbrowser dillo;

* unlike every other browser, dillo does NOT send a trailing "\r\n"
   after the terminating "--"; turns out that dillo got this right
   and every other browser didn't, fun

* dillo announces the boundary in quotes, which is spec-optional

the multipart-parser is now 2% slower
2026-01-21 03:19:32 +00:00
ed 78f6855f08 update pkgs to 1.20.2 2026-01-19 01:31:57 +00:00
ed cab9feb225 v1.20.2 2026-01-19 01:26:37 +00:00
ed e752005543 rcm: config sets defaults;
also rename ren/rdb to reduce probability of
localstorage conflicts with other softwares
2026-01-19 00:28:00 +00:00
ed 206b1752e6 mtl new strings 2026-01-19 00:09:55 +00:00
ed df8b395380 ( ´_ゝ`) 2026-01-18 23:22:13 +00:00
ed 67c5d8dae4 u2c: server-offline timeout; closes #1197 2026-01-18 23:17:37 +00:00
ed c17c3be008 wo_up_readme according to volflags;
now that the filenames of logues/readmes can be customized,
match against the configured names rather than the defaults
2026-01-18 23:10:11 +00:00
ed 22b9c283d1 dsel: boomer-proofing
s/boomer/ed/
2026-01-18 22:22:40 +00:00
ed 60ceea4b42 add nospawn, assert_root 2026-01-18 22:15:34 +00:00
ed 826e84c8ec add --flo (logfile format) 2026-01-18 21:03:43 +00:00
ed d90d021969 readme: windows pathlen warning 2026-01-18 20:27:09 +00:00
ed d925553810 override domain for uploads (closes #255);
`up-site` will override the scheme and domain (https://example.com/)
in the links to newly uploaded files, making it possible to upload a
file from a LAN IP while obtaining an external URL in return
2026-01-18 00:30:46 +00:00
edandmechabubba 41d3bae929 override domain for shares (closes #1211);
`shr-site` will override the scheme and domain (https://example.com/)
in the link to a newly created share, making it possible to create a
share from a LAN IP while obtaining an external URL in return

---------

Co-authored-by: mechabubba <stevenvanniisprettycool@gmail.com>
2026-01-17 23:58:57 +00:00
stackxp ffb2560322 rcm: add share options (#1216) 2026-01-17 23:24:53 +00:00
ed 40fb263097 rephrase "see serverlog" 2026-01-17 20:35:06 +00:00
ed 4e67b467a9 don't mkdir jumpvol realpath 2026-01-17 20:23:31 +00:00
exci 72c59405e7 dsel: fix bugs, improve selection (#1214)
* additive/subtractive select (hold shift/alt while releasing LMB)
* mobile support
2026-01-16 21:17:01 +00:00
ed 5d7cf80ff0 dsel: default on desktop + ie11 2026-01-16 01:11:26 +00:00
exci 3e3228e0f6 add drag-selection to gridview (#1212) 2026-01-16 00:52:19 +00:00
ed 930e864239 idxh: ensure trailing slash 2026-01-15 22:56:10 +00:00
ed bef07720d9 dedup: use chmod/chown volflags; closes #1203 2026-01-15 19:19:27 +00:00
ed 5b3191b495 add warning about traefik bug (#1205) 2026-01-15 17:56:55 +00:00
hackysphereanded d32704ed37 rcm: search-results fix (#1206)
* fix: show rcm on empty space and make search header not selectable

---------

Co-authored-by: ed <s@ocv.me>
2026-01-15 08:39:03 +00:00
hackysphereanded 04f612ff63 rcm in search results; closes #1202 (#1198)
* fix: make right-click menu work in search view
* fix: allow for markdown files to be opened in viewer when in search view

---------

Co-authored-by: ed <s@ocv.me>
2026-01-14 23:53:07 +00:00
ed 66750391ae fix jumpvol ?ls 2026-01-14 23:46:35 +00:00
ed c46cd7f57a add html id for css; closes #1200 2026-01-13 20:38:46 +00:00
ed 8240ef6151 index xattrs as tags; closes #134 2026-01-13 02:50:32 +00:00
ed bc24604a83 dl-button: skip folders (would give html) 2026-01-12 18:46:47 +00:00
ed 266489113a fix unlistc* for filevols 2026-01-12 18:06:05 +00:00
Toast d5a8a34bca NixOS: misc module documentation improvements (#1194)
* nixos: use mkPackageOption instead of mkOption for the package option

This gets rid of a warning when trying to build a system with
documentation.nixos.includeAllModules enabled

* nixos: don't include package path in option descriptions

See https://github.com/9001/copyparty/pull/1193, this actually fixes the issue

* nixos: fix formatting of services.copyparty.volumes.<name>.access

List will now show up properly on the nixos manual!

* nixos: add or improve inline code blocks in option descriptions
2026-01-12 16:43:18 +01:00
Toast 9d223d6ca7 nixos: use mkPackageOption instead of mkOption (#1193)
This gets rid of a warning when trying to build a system with
documentation.nixos.includeAllModules enabled
2026-01-11 23:08:57 +00:00
ed caf831fc28 fix ipu nullmapping; closes #1191
regression added in v1.19.21 / 79e1078671
2026-01-11 17:10:16 +00:00
ed ae56f3bdae readme: rephrase -j0 advice 2026-01-11 16:52:19 +01:00
stackxp 25a8b96fd1 rcm: add rename, some fixes (#1184)
Signed-off-by: stackxp <170874486+stackxp@users.noreply.github.com>
2026-01-11 16:45:39 +01:00
ed 7357d46f43 http415 on failed transcode; closes #1179 2026-01-10 22:38:23 +00:00
ed 3aebfabd5c mtag: truncate stderr 2026-01-10 22:16:50 +00:00
ed 8914f0af70 docs: suggest modern ipa syntax (#1181) 2026-01-09 20:45:00 +00:00
ed a9ae6d5131 update pkgs to 1.20.1 2026-01-09 00:57:31 +00:00
ed ecd4fcc1b3 v1.20.1 2026-01-09 00:52:35 +00:00
ed 738a419b2b append: only allow with reflink or !dedup;
un-dedup of target file is likely too expensive, and
relying on up2k.db to do it conditionally too dangerous
2026-01-09 00:29:44 +00:00
ed 3a16d3461d rcm: fix copy-link filekeys 2026-01-08 23:29:58 +00:00
ed 3a52096205 octalize 2026-01-08 23:27:59 +00:00
ed 87a5c22a34 per-protocol ip binds; closes #1176 2026-01-07 23:27:03 +00:00
ed 7d6e59f347 enable japanese translation 2026-01-07 22:35:12 +00:00
tkymmm b918b592ca Add Japanese translation (#1174)
Signed-off-by: tkymmm <136296842+tkymmm@users.noreply.github.com>
2026-01-07 22:32:02 +00:00
ed cbeb439ad2 mtl new strings 2026-01-07 22:25:45 +00:00
ed 74a6ce1c1e i18n: update translation template 2026-01-07 22:17:00 +00:00
ed 2f4a30b620 sftp: fix default chmod (#1170) 2026-01-07 21:40:07 +00:00
ed 6c41bac6d2 sftp: fix nested write-only folders (could not browse) 2026-01-07 21:34:50 +00:00
ed 8c9e1016de sftp: allow ENOENT from rm (closes #1170);
some sftp clients will try to rm a file before creating it,
expecting ENOENT even in write-only folders
2026-01-07 20:58:23 +00:00
ed 9030828494 sftp: loosen stat restrictions (#1170);
some sftp clients always expect correct stat results, even in
write-only folders, so this slight info-leak must be allowed
2026-01-07 20:56:51 +00:00
ed 13055c6451 sftp: improve logging 2026-01-07 20:36:34 +00:00
ed 038af50777 shares: fix vmaxb 2026-01-05 18:32:39 +00:00
Raphaël Deschênes 7ffb1fd0ca contrib/nginx: fix rp-loc example (#1167)
Signed-off-by: Raphaël Deschênes <raphdf201@hotmail.com>
2026-01-04 18:56:52 +00:00
ed 364f74a1bd iOS/ashell: fix howto + add beta 2026-01-04 12:39:39 +00:00
seaslug feabbf3e6a rcm: gridview support (#1164) 2026-01-04 12:18:57 +00:00
ed 5b2c84752d update pkgs to 1.20.0 2026-01-02 23:09:37 +00:00
ed cce1210792 v1.20.0 2026-01-02 00:07:31 +00:00
ed e55e5a4585 toggle unlist with dotfiles-button 2026-01-02 20:47:24 +00:00
ed af3f777ec9 detect browsers failing to load various js files 2026-01-02 20:16:38 +00:00
ed 80a3749238 splashpage: don't add trailing slash to filevols;
it prevented opening the link on Windows CE 4.20
(internet explorer 4.01)
2026-01-02 17:55:47 +00:00
ed 2f3591d036 new-file: warn on missing required .md$ 2026-01-02 16:59:18 +00:00
ed 65391a9d8c more linter fixing 2026-01-02 16:56:03 +00:00
stackxpandstackxp fd141c61b5 rcm: more fixes (#1160)
Co-authored-by: stackxp <tillijungblut@gmail.com>
2026-01-02 15:35:53 +00:00
ed 3ee91dffad linter denoise 2026-01-02 15:23:31 +00:00
ed ec7ea30951 sftp: misc fixes + add to docker-im 2026-01-02 15:18:11 +00:00
stackxpandstackxp 4e9cf95e8d Fix right-click menu (#1159)
Co-authored-by: stackxp <tillijungblut@gmail.com>
2026-01-02 14:23:39 +01:00
AppleTheGolden 3bf80c8152 update german translations (#1158) 2026-01-02 12:37:54 +01:00
ed 5b89a2e3b2 config-files can extend groups 2026-01-02 01:39:51 +00:00
ed f81d80bcad option to change the "pw" header/uparam name;
useful to force basic-auth and such
2026-01-01 23:59:16 +00:00
ed f08cb25ccc load idp groups also for native accounts;
previously, if a native user was authed by an idp instead of the
config password, it would not load and register the idp groups
2026-01-01 21:50:25 +00:00
ed 7d7a1510fb download-as-zip: option to skip dotfiles 2026-01-01 21:41:11 +00:00
ed 63d8e5a033 append to existing files with PUT 2026-01-01 20:32:33 +00:00
ed ec51d3241c mtl new strings 2026-01-01 18:02:20 +01:00
ed 85639ad2cd button to skip conflicts on copy/move; closes #1124 2026-01-01 16:04:06 +01:00
ed 05a4472075 rcm: fixes;
* reload with ?ls
* menu positioning
* close with ESC
2026-01-01 14:33:56 +01:00
stackxp 82c496092f Add right-click menu (#1135) 2026-01-01 13:44:55 +01:00
ed 8551472bf0 add workaround for #1147;
certain browser-extensions can erroneously unmap modals
2026-01-01 13:38:05 +01:00
ed d1ddcb19f5 fix dks in grid; closes #1157 2026-01-01 13:13:10 +01:00
ed 39c3ccc2eb it runs on SGI IRIX (more 39c3 memes) 2025-12-31 01:45:19 +00:00
ed 4714c2fa5a add sftp server (powered by 39c3) 2025-12-30 23:38:54 +00:00
Leo Developer 120fdfb257 u2c: add basic-auth support (#1151)
u2c: add --ba argument to use basicauth instead of, or in addition to, the PW header.

This allows using u2c.py with a copyparty server behind a reverseproxy which enforces authentication through basicauth.

Also expands the BadAuth check to status 401.

Closes #1150
2025-12-29 23:42:02 +00:00
ed 4642d32366 dedicated tcp-port for tricky webdav clients;
there are webdav-clients (for example zotero) which fully pretend
to be a graphical webbrowser, going as far as faking the firefox
user-agent, which means they get the graphical login-page
instead of 401 (basic-authentication challenge)

these webdav-clients unfortunately also refuse to send credentials
unless they get 401'd, so until now it was impossible to connect them

the obvious solution of adding a suffix to
links in PROPFIND responses is a nonstarter;

* windows-webdav ignores the <displayname> property and shows the
   <href> as the filename, so this would show up in windows explorer
   and probably make most file operations impossible

* rclone is the opposite; ignores the <href> property (so it wouldn't
   even see the suffix) and builds its own URL from the <displayname>

so we need a new weapon:

gloabl-option dav-port makes copyparty listen on another port which
is dedicated to webdav-clients that otherwise don't look the part

global-option p-nodav is the opposite; tags a listening-port as
only accepting connections from graphical browsers, just in case

closes #1142
2025-12-26 17:21:58 +00:00
ed 2c26aecd87 descript.ion for folders; closes #1127 2025-12-24 11:21:58 +00:00
ed d8c732469b show vmaxb instead of fs total-size (#1120) 2025-12-24 11:06:49 +00:00
ed 4c73704ce7 fix ui-notree; closes #1123 2025-12-23 22:49:04 +00:00
ed e0845b2363 use vmaxb in the web-ui too;
also caches the volume usage to restore most of the performance lost in 511dc01615
2025-12-23 22:11:38 +00:00
Rabid 511dc01615 webdav: reply with vmaxb as disk size (#1120)
if vmaxb smaller than available disk space, then reply with vmaxb instead
2025-12-23 20:06:36 +00:00
AppleTheGolden fa32e15958 up2k: add "skip if file exists" (#1128) 2025-12-23 19:08:29 +00:00
ed 2d1d295a4d windows: fix download-as-zip;
zipfiles generated on windows were flattened
(slash was substituted with fullwidth)
2025-12-23 10:52:50 +00:00
ed c82a3cb226 this fell off 485c60cf25 2025-12-21 17:07:58 +00:00
ed 485c60cf25 bbox: copy rotation to next pic 2025-12-21 16:06:27 +00:00
ed c0e167fd97 bbox: fix rotate w/ zoom 2025-12-21 15:34:46 +00:00
ed 7bfd370b6c mte can enable: w .up_at up_by up_ip 2025-12-20 15:54:42 +00:00
ed 1f6e811674 show perms in ?h&ls + fix unlistc* 2025-12-19 21:54:19 +00:00
Vincent Milum Jr 0e6b167167 FreeBSD is Python 3.11 now (#1121)
Signed-off-by: Vincent Milum Jr <git@darkain.com>
2025-12-18 09:17:38 +01:00
ed 519bfe1f0b update pkgs to 1.19.23 2025-12-17 00:18:55 +00:00
ed d006cecaef v1.19.23 2025-12-17 00:14:13 +00:00
ed b6c2ec15db fix @acct without idp-h-grp; closes #1113 2025-12-17 00:01:23 +00:00
ed 0b6d2d2424 safari: workaround another apple bug (closes #1111);
seemingly as of iOS / macos 26.1, safari started requesting
favicons -- specifically only favicons -- with the incorrect
browser context (they probably forgot to initialize something)

instead of the correct user-agent, it would send:
* iOS: NetworkingExtension/8623.1.14.10.9
* macos: com.apple.WebKit.Networking/21623.1.14.11.9

further, it would NOT send any SameSite=Strict cookies,
which the session-cookie is (for good reason)

putting these two together, safari now looks like a webdav client,
and copyparty sends the only appropriate response (http 401),
resulting in a basic-authentication popup

left with no good options, this is what we can do to mitigate:

* add a new option --ua-nodav which is a regex of user-agents
   which are definitely not webdav clients, as macos-finder still
   flipflops between WebDAVLib/1.3 and WebDAVFS/3.0.0 like normal

* use the "js=y" cookie as another flag that this is a webbrowser

merry christmas
2025-12-16 22:38:51 +00:00
ed 8d46cf1823 login-ui password max-length hint; closes #1029 2025-12-16 21:54:36 +00:00
ed c8f3b4ef05 warning in controlpanel for rproxy misconfig 2025-12-16 21:31:32 +00:00
ed 9c64788d43 add x-forwarded-proto fallback (closes #1110);
some reverseproxies do not include a compatible alternative to
x-forwarded-proto by default, while also lacking the option to
set custom headers

add --xf-proto-fb to set a fixed protocol to assume
2025-12-16 21:15:44 +00:00
ed 5e1d9a58d8 simplify idp-groups with spaces 2025-12-16 21:07:09 +00:00
ed 336842192c add --ipar (reverseproxy-capable ipa); closes #1109 2025-12-16 20:38:37 +00:00
ed d12cf9aee1 u2c: fix deprecation + add -teh 2025-12-16 20:27:55 +00:00
ed db38cb2f79 mtl new strings 2025-12-16 20:27:09 +00:00
ed d4a9787c6c enable vietnamese translation 2025-12-16 20:17:55 +00:00
thatfrozenfrog b60eb3f01a add Vietnamese translation (#1080) 2025-12-16 19:52:07 +00:00
ed 3476d5e6d9 update pkgs to 1.19.22 2025-12-14 23:07:26 +00:00
ed a3eec23cef v1.19.22 2025-12-14 23:04:36 +00:00
ed 2e7390a4c5 new-file: suggest .md rather than .txt 2025-12-14 22:19:57 +00:00
ed e0b04d9c16 webdeps: dompurify-3.3.1 2025-12-14 22:18:43 +00:00
ed 9e64fe02f9 deps: copyparty.exe: python-3.13.11 2025-12-14 22:15:53 +00:00
ed 67ddc64171 mtag: replace keyfinder-py with keyfinder-cli;
died in alpine 3.23 due to ffmpeg8
2025-12-14 22:04:46 +00:00
ed 56e15009c7 controlpanel: use english for untranslated strings 2025-12-14 20:14:05 +00:00
08474dbe14 reject blank password in login ui (#1105)
inlines css in msg.html to remove a roundtrip; response now requires
multiple tcp-packets but probably always did realistically (https)

Co-authored-by: stackxp <tillijungblut@gmail.com>
Co-authored-by: ed <s@ocv.me>
2025-12-14 20:05:22 +00:00
ed 5a1f0a330c readme: faq: volflags, volumes 2025-12-14 18:29:53 +00:00
ed 3bc0bf19b0 cache-control volflag; closes #964 2025-12-14 18:28:53 +00:00
ed efc6a09dd3 allow existing blank chpw.json (closes #1038);
previously, would crash on startup if chpw.json exists and is blank,
because valid json was enforced

now allowing a blank initial file to match the behavior of sqlite
2025-12-14 17:24:54 +00:00
ed 921954037b warn that rss requires e2d; closes #1104 2025-12-14 17:17:44 +00:00
ed fecc3fd507 rename metadata-property "date" to "tdate";
"date" is reserved for the last-modified-timestamp of each file

if extraction of the audio metadata property "date" was enabled
(not default), this would have collided; rename the audio prop

discovered thanks to #1053
2025-12-14 00:15:12 +00:00
edandDawson Jeane 5e85e3d628 rss: title/description templating; closes #1047
also closes #1053, a PR which inspired this commit heavily
(slightly different approach for flexibility and performance)

Co-authored-by: Dawson Jeane <dawsonmjeane@gmail.com>
2025-12-14 00:06:54 +00:00
ed 965a4a6949 logging: date format; closes #1049 2025-12-13 22:35:55 +00:00
ed 7f82189da9 readme: archlinux systemd; closes #1070 2025-12-13 22:17:15 +00:00
ed 14bef85b87 custom logue/md names; closes #1068, closes #1089 2025-12-13 22:05:29 +00:00
ed 594ec39481 fix ipu with idp users; closes #1094 2025-12-13 20:09:08 +00:00
ed ba017f7b53 only use fs-legal chars in names (closes #1010);
uploading a folder named COMPLE:X into exfat on linux would fail
because exfat behaves like windows, rejecting <>:|?*"\/

this would also fail on windows, but then due to
sanitize_fn being overly aggressive

fix this by detecting filesystem traits on startup and
also translating vpath early on windows
2025-12-13 19:44:56 +00:00
ed 3bbed1bc46 fstab: deref fuseblk to real fs 2025-12-13 16:21:49 +00:00
ed 4b0064b209 discard rejected connection 2025-12-13 14:41:16 +00:00
ed e440578cae apply ?nosrvi to #srv_info2 too; closes #1102 2025-12-12 23:35:21 +00:00
ed 1a9d4c04d5 mediaplayer: cache now-playing tags;
fixes copy-to-irc after navigating to another folder
2025-12-12 22:52:18 +00:00
ed 8e2fb05ab8 audioplayer: fix preload in huge folders;
it would skip to next folder instead of untruncating
2025-12-12 22:29:33 +00:00
ed ca6c4deaac delete thumbnail-cache if settings change 2025-12-12 21:25:33 +00:00
ed a1cbac0252 option to set thumbnail quality (#1092);
plus these fixes:

* adds a previously missed libvips optimization,
   giving much smaller files at the same quality

* try to align the quality-scale of each backend
   (pillow, libvips, ffmpeg) by filesize
2025-12-12 07:51:01 +00:00
ed 1b222fb576 revert to X-Forwarded-Host being optional;
turns out reverseproxies keeping the initial Host value is the
far more common case; requiring X-Forwarded-Host is a bad idea

partially reverts ad45de9441
2025-12-11 22:15:46 +00:00
ed ce2eeba226 custom ban-message 2025-12-11 21:38:36 +00:00
ed ad45de9441 enforce x-forwarded-host when reverse-proxied;
if x-forwarded-for is present, then also require
x-forwarded-host and x-forwarded-proto

avoids displaying subtly-incorrect values on the connect-page
and instead shows blatantly-incorrect values ("example.com")

the headernames x-forwarded-host and x-forwarded-proto can
be configured with global-options xf-host and xf-proto
2025-12-11 21:32:43 +00:00
ed 7d526eaba3 fix termsz on windows 2025-12-11 16:41:55 +00:00
ed 1b0eb45032 synology: hide @eaDir folders everywhere 2025-12-11 16:26:48 +00:00
emilia a86983928c fix podman-systemd typo; closes #1088 (#988)
Signed-off-by: emilia <46273791+emiliatheworst@users.noreply.github.com>
2025-12-09 19:46:14 +00:00
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
shelvacuandShelvacu 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 Ismoanded 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
edandCarson Coder 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
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 Boreckianddai 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
exciandicxes 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 Surettanded 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 DebreandKaleb 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
AppleTheGoldenanded 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
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
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 Pelcanded 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
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
mati1210anded 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
chamdimanded 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
exciandicxes 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
Adamanded 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: https://github.com/NixOS/nixpkgs/blob/3896a0c0e22c8834e8caa297b6e508805c89d1b4/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
243 changed files with 36094 additions and 5555 deletions

No files matched your search

+10 -4
View File
@@ -7,9 +7,9 @@ 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
a description of what the bug is
@@ -33,7 +33,7 @@ remove the ones that are not relevant:
### Server details (if you're NOT using docker/podman)
remove the ones that are not relevant:
* **server OS / version:**
* **what copyparty did you grab:** (sfx/exe/pip/aur/...)
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
* **how you're running it:** (in a terminal, as a systemd-service, ...)
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
* **copyparty arguments and/or config-file:**
@@ -44,5 +44,11 @@ if the issue is possibly on the client-side, then mention some of the following:
* OS version:
* browser version:
### 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
+3 -1
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
View File
@@ -43,3 +43,7 @@ scripts/docker/*.err
# nix build output link
result
result-*
# IDEA config
.idea/
+34 -18
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
},
}
]
}
}
+17 -4
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,7 @@ 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
+587 -59
View File
File diff suppressed because it is too large. Load diff
+1 -3
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
+1 -1
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
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
bwrap \
--unshare-all \
@@ -9,7 +9,7 @@ bwrap \
--dev-bind /dev /dev \
--dir /tmp \
--dir /var \
--bind $(pwd) $(pwd) \
--bind "$(pwd)" "$(pwd)" \
--share-net \
--die-with-parent \
--file 11 /etc/passwd \
+2 -2
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()
+1
View File
@@ -21,6 +21,7 @@ each plugin must define a `main()` which takes 3 arguments;
## on404
* [redirect.py](redirect.py) sends an HTTP 301 or 302, redirecting the client to another page/file
* [randpic.py](randpic.py) redirects `/foo/bar/randpic.jpg` to a random pic in `/foo/bar/`
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
+35
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"
+2 -2
View File
@@ -46,7 +46,7 @@ def main(cli, vn, rem):
# 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)
# send_http_301_permanent_redirect(cli, new_path)
# send_errorpage_with_redirect_link(cli, new_path)
return "true"
+17
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
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}
+1 -1
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
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,))
+60
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()
+72
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}
+5 -2
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
+11 -6
View File
@@ -1,11 +1,13 @@
// 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);
@@ -19,11 +21,14 @@ function add_eject_2(a) {
return;
var v = aw[2],
k = 'umount_' + v,
o = ebi(k);
k = 'umount_' + v;
if (o)
for (var b = 0; b < 9; b++) {
var o = ebi(k);
if (!o)
break;
o.parentNode.removeChild(o);
}
a.appendChild(mknod('span', k, '⏏'), a);
o = ebi(k);
@@ -40,7 +45,7 @@ function add_eject_2(a) {
};
function add_eject() {
var o = QSA('#treeul a[href^="/usb/"]');
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]);
};
+11 -4
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
"""
@@ -28,14 +29,20 @@ 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/egon/" + 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")
# if you're running copyparty as root (thx for the faith)
+100
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()
+4 -1
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()
+5
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%')";`
+19 -3
View File
@@ -4,13 +4,19 @@ import os
import sys
import tempfile
import subprocess as sp
import keyfinder
try:
import keyfinder
PKF = True
except:
PKF = False
from copyparty.util import fsenc
"""
dep: github/mixxxdj/libkeyfinder
dep: pypi/keyfinder
dep: pypi/keyfinder -OR- EvanPurkhiser/keyfinder-cli
dep: ffmpeg
"""
@@ -35,7 +41,17 @@ def det(tf):
])
# fmt: on
print(keyfinder.key(tf).camelot())
if PKF:
print(keyfinder.key(tf).camelot())
else:
# fmt: off
sp.check_call([
b"keyfinder-cli",
b"-n",
b"camelot",
fsenc(tf)
])
# fmt: on
def main():
+5 -1
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
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()
+1 -1
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,
+1 -1
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",
+12 -2
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=
@@ -153,6 +155,11 @@ install_keyfinder() {
return
}
(cat /etc/alpine-release || echo a) 2>&1 | grep -E '3\.2[3-9]' && {
echo "alpine too new; ffmpeg8 is keyfinder-py incompat; giving up"
return
}
cd "$td"
github_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest
ls -al
@@ -186,12 +193,15 @@ install_keyfinder() {
echo "so not found at $sop"
exit 1
}
x=${-//[^x]/}; set -x; cat /etc/alpine-release || true
# 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
+1 -1
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)
+22 -13
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.2"
S_BUILD_DT = "2025-12-16"
"""
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:
@@ -284,8 +284,8 @@ class Gateway(object):
if ar.td:
self.ssl_context = ssl._create_unverified_context()
elif ar.te:
self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
self.ssl_context.load_verify_locations(ar.te)
self.ssl_context = ssl.create_default_context(cafile=ar.te)
self.ssl_context.check_hostname = ar.teh
self.conns = {}
@@ -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")
@@ -1157,6 +1165,7 @@ def main():
ap2 = ap.add_argument_group("https/TLS")
ap2.add_argument("-te", metavar="PEMFILE", help="certificate to expect/verify")
ap2.add_argument("-teh", action="store_true", help="require correct hostname in -te cert")
ap2.add_argument("-td", action="store_true", help="disable certificate check")
ap2 = ap.add_argument_group("cache/perf")
+1 -1
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
+63 -30
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "2.9"
S_BUILD_DT = "2025-01-27"
S_VERSION = "2.19"
S_BUILD_DT = "2026-01-18"
"""
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"
@@ -98,7 +100,7 @@ except:
ub64enc = base64.urlsafe_b64encode
class BadAuth(Exception):
class Fatal(Exception):
pass
@@ -163,8 +165,8 @@ class HCli(object):
elif self.verify is True:
self.ctx = None
else:
self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
self.ctx.load_verify_locations(self.verify)
self.ctx = ssl.create_default_context(cafile=self.verify)
self.ctx.check_hostname = ar.teh
self.base_hdrs = {
"Accept": "*/*",
@@ -194,6 +196,8 @@ class HCli(object):
hdrs.update(self.base_hdrs)
if self.ar.a:
hdrs["PW"] = self.ar.a
if self.ar.ba:
hdrs["Authorization"] = self.ar.ba
if ctype:
hdrs["Content-Type"] = ctype
if meth == "POST" and CLEN not in hdrs:
@@ -230,6 +234,7 @@ class HCli(object):
MJ = "application/json"
MO = "application/octet-stream"
MM = "application/x-www-form-urlencoded"
CLEN = "Content-Length"
web = None # type: HCli
@@ -489,6 +494,12 @@ print = safe_print if VT100 else flushing_print
def termsize():
try:
w, h = os.get_terminal_size()
return w, h
except:
pass
env = os.environ
def ioctl_GWINSZ(fd):
@@ -588,9 +599,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:
@@ -599,8 +611,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:
@@ -675,7 +688,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
@@ -807,7 +820,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
@@ -820,10 +835,15 @@ def handshake(ar, file, search):
url = ""
url = ar.vtop + url
t0 = time.time()
tmax = t0 + ar.t_hs
while True:
sc = 600
txt = ""
t0 = time.time()
t1 = time.time()
if t1 >= tmax:
print("\nERROR: server offline for longer than --t-hs; giving up")
raise Fatal()
try:
zs = json.dumps(req, separators=(",\n", ": "))
sc, txt = web.req("POST", url, {}, zs.encode("utf-8"), MJ)
@@ -844,13 +864,13 @@ def handshake(ar, file, search):
return [], False
elif sc == 409 or "<pre>upload rejected, file already exists" in txt:
return [], False
elif sc == 403:
elif sc == 403 or sc == 401:
print("\nERROR: login required, or wrong password:\n%s" % (txt,))
raise BadAuth()
raise Fatal()
t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f td=%.3f\n %s\n\n"
t = "handshake failed, retrying: %s\n t0=%.3f t1=%.3f t2=%.3f td1=%.3f td2=%.3f\n %s\n\n"
now = time.time()
eprint(t % (file.name, t0, now, now - t0, em))
eprint(t % (file.name, t0, t1, now, now - t0, now - t1, em))
time.sleep(ar.cd)
try:
@@ -973,6 +993,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()
@@ -1009,6 +1030,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):
@@ -1030,7 +1056,7 @@ class Ctl(object):
print(" hs...")
try:
hs, _ = handshake(self.ar, file, search)
except BadAuth:
except Fatal:
sys.exit(1)
if search:
@@ -1219,9 +1245,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]
@@ -1287,7 +1311,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,
@@ -1337,7 +1361,7 @@ class Ctl(object):
try:
hs, sprs = handshake(self.ar, file, search)
except BadAuth:
except Fatal:
self.panik = 1
break
@@ -1449,6 +1473,7 @@ class Ctl(object):
file = fsl.file
cids = fsl.cids
self.last_file = file
with self.mutex:
if not self.uploader_busy:
@@ -1523,21 +1548,23 @@ 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("-a", metavar="PASSWD", default="", help="password (or $filepath) for copyparty (is sent in header 'PW')")
ap.add_argument("--ba", metavar="PASSWD", default="", help="password (or $filepath) for basic-auth (usually not necessary)")
ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
ap.add_argument("-x", type=unicode, metavar="REGEX", action="append", help="skip file if filesystem-abspath matches REGEX (option can be repeated), example: '.*/\\.hist/.*'")
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")
@@ -1569,11 +1596,13 @@ source file/folder selection uses rsync syntax, meaning that:
ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)")
ap.add_argument("--cxp", type=float, metavar="SEC", default=57, help="assume http connections expired after SEConds")
ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload")
ap.add_argument("--t-hs", type=float, metavar="SEC", default=186, help="crash if handshakes fail due to server-offline for this long")
ap.add_argument("--safe", action="store_true", help="use simple fallback approach")
ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")
ap = app.add_argument_group("tls")
ap.add_argument("-te", metavar="PATH", help="path to ca.pem or cert.pem to expect/verify")
ap.add_argument("-teh", action="store_true", help="require correct hostname in -te cert")
ap.add_argument("-td", action="store_true", help="disable certificate check")
# fmt: on
@@ -1669,11 +1698,15 @@ source file/folder selection uses rsync syntax, meaning that:
print("\n\n %s\n\n" % (t,))
raise
if ar.a and ar.a.startswith("$"):
fn = ar.a[1:]
print("reading password from file [{0}]".format(fn))
with open(fn, "rb") as f:
ar.a = f.read().decode("utf-8").strip()
for k in ("a", "ba"):
zs = getattr(ar, k)
if zs.startswith("$"):
print("reading password from file [%s]" % (zs[1:],))
with open(zs[1:], "rb") as f:
setattr(ar, k, f.read().decode("utf-8").strip())
if ar.ba:
ar.ba = "Basic " + base64.b64encode(ar.ba.encode("utf-8")).decode("utf-8")
for n in range(ar.rh):
try:
+2
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)
+9
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
+25 -6
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/er 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;
}
}
+293 -167
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,31 +48,62 @@ 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";
package = mkOption {
type = types.package;
default = pkgs.copyparty;
defaultText = "pkgs.copyparty";
description = ''
package = mkPackageOption pkgs "copyparty" {
extraDescription = ''
Package of the application to run, exposed for overriding purposes.
'';
};
mkHashWrapper = mkOption {
type = types.bool;
default = true;
description = ''
Make a shell script wrapper called {command}`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;
@@ -78,34 +114,48 @@ in {
type = types.attrs;
description = ''
Global settings to apply.
Directly maps to values in the [global] section of the copyparty config.
See `${getExe cfg.package} --help` for more details.
Directly maps to values in the `[global]` section of the copyparty config.
Cannot set "c" or "hist", those are set by this module.
See {command}`copyparty --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 +167,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 {command}`copyparty --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 +274,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
'')
];
}
);
}
+28 -36
View File
@@ -1,57 +1,49 @@
# 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.12"
pkgver="1.20.2"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, 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-paramiko: sftp server",
"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=("b5b65103198a3dd8a3f9b15c3d6aff6c21147bf87627ceacc64205493c248997")
backup=("etc/${pkgname}/copyparty.conf" )
sha256sums=("f4fc7a53a6d23163426f4f0f407c7aa53984e0b7a7b0b33700464bc7d7f3bd95")
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 "┗━━━━━━━━━━━━━━━──-"
}
+45
View File
@@ -0,0 +1,45 @@
# Contributor: Beethoven <beethovenisadog@protonmail.com>
pkgname=copyparty
pkgver=1.20.2
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, 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-paramiko: sftp server"
"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=("f4fc7a53a6d23163426f4f0f407c7aa53984e0b7a7b0b33700464bc7d7f3bd95")
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"
}
@@ -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
File renamed without changes.
+142 -40
View File
@@ -1,45 +1,144 @@
{ 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,
paramiko,
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 SFTP server
withSFTP ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# enable FTP server
withFTP ? true,
# 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 withSFTP paramiko
++ 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 +146,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, SFTP,
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"
'';
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.16.12/copyparty-sfx.py",
"version": "1.16.12",
"hash": "sha256-gZZqd88/8PEseVtWspocqrWV7Ck8YQAhcsa4ED3F4JU="
"url": "https://github.com/9001/copyparty/releases/download/v1.20.2/copyparty-1.20.2.tar.gz",
"version": "1.20.2",
"hash": "sha256-9Px6U6bSMWNCb08PQHx6pTmE4LensLM3AEZLx9fzvZU="
}
+12 -11
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
+32
View File
@@ -0,0 +1,32 @@
final: prev:
let
fullAttrs = {
withHashedPasswords = true;
withCertgen = true;
withThumbnails = true;
withFastThumbnails = true;
withMediaProcessing = true;
withBasicAudioMetadata = true;
withZeroMQ = true;
withSFTP = 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 { };
};
};
}
+30
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;
};
}
+5
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="
}
+50
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()
+62
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, SFTP, 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-paramiko, python-impacket
%description
Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP, 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
+6
View File
@@ -39,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
+17
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 = `
+3 -3
View File
@@ -101,10 +101,10 @@
function our_hotkey_handler(e) {
// bail if either ALT, CTRL, or SHIFT is pressed
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
if (anymod(e))
return main_hotkey_handler(e); // let copyparty handle this keystroke
var key_name = (e.code || e.key) + '',
var keycode = (e.key || e.code) + '',
ae = document.activeElement,
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
@@ -114,7 +114,7 @@
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
return main_hotkey_handler(e); // let copyparty handle this keystroke
if (key_name == 'KeyW') {
if (keycode == 'w' || keycode == 'KeyW') {
// okay, this one's for us... do the thing
action_to_perform();
return ev(e);
+173
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/containers/systemd/ /etc/copyparty/
sudo cp -v copyparty.container /etc/containers/systemd/
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.
+36
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
@@ -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
+1 -1
View File
@@ -10,7 +10,7 @@ name="copyparty"
rcvar="copyparty_enable"
copyparty_user="copyparty"
copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit
copyparty_command="/usr/local/bin/python3.9 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
copyparty_command="/usr/local/bin/python3.11 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}"
pidfile="/var/run/copyparty/${name}.pid"
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f ${copyparty_command}"
+71
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 | 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
+26
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
+8 -37
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
+42
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
+30
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
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`
+38
View File
@@ -0,0 +1,38 @@
# this will start `/usr/bin/copyparty`
# in a chroot, preventing accidental access elsewhere,
# and read copyparty config from `/etc/copyparty/copyparty.conf`
#
# expose additional filesystem locations to copyparty
# by listing them between the last `%i` and `--`
#
# `%i %i` = user/group to run copyparty as; can be IDs (1000 1000)
# the %i refers to whatever you put after the prisonparty@
# so with prisonparty@foo.service, %i == foo
#
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=prisonparty
WorkingDirectory=/var/lib/copyparty-jail
Environment=PYTHONUNBUFFERED=x
Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf
ExecReload=/bin/kill -s USR1 $MAINPID
# user to run as + where the TLS certificate is (if any)
User=%i
Environment=XDG_CONFIG_HOME=/home/%i/.config
# run copyparty
ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \
/etc/copyparty \
-- \
/usr/bin/python3 /usr/bin/copyparty
[Install]
WantedBy=multi-user.target
+107
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()
+31 -6
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
@@ -89,8 +86,8 @@ web/md2.js
web/mde.css
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 +99,47 @@ 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/jpn.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/tl/vie.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()
+610 -215
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 16, 13)
CODENAME = "COPYparty"
BUILD_DT = (2025, 2, 13)
VERSION = (1, 20, 3)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 1, 21)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
+1076 -164
View File
File diff suppressed because it is too large. Load diff
+75 -10
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:
+3
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
-1
View File
@@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals
import argparse
import traceback
from queue import Queue
+21 -8
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:
+142 -3
View File
@@ -19,15 +19,20 @@ 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",
"vol_nospawn": "nospawn",
"vol_or_crash": "assert_root",
}
for k in (
"dedup",
"dlni",
"dotsrch",
"e2d",
"e2ds",
@@ -43,17 +48,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
@@ -62,6 +85,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",
@@ -72,14 +96,29 @@ def vf_vmap() -> dict[str, str]:
"th_x3": "th3x",
}
for k in (
"apnd_who",
"bup_ck",
"cachectl",
"casechk",
"chmod_d",
"chmod_f",
"dbd",
"db_xattr",
"du_who",
"epilogues",
"ufavico",
"forget_ip",
"fsnt",
"hsortn",
"html_head",
"html_head_s",
"lg_sbf",
"md_sbf",
"lg_sba",
"md_sba",
"md_hist",
"nrand",
"u2ow",
"og_desc",
"og_site",
"og_th",
@@ -89,15 +128,38 @@ def vf_vmap() -> dict[str, str]:
"og_title_i",
"og_tpl",
"og_ua",
"opds_exts",
"prologues",
"preadmes",
"put_ck",
"put_name",
"readmes",
"mv_retry",
"rm_retry",
"rss_sort",
"rss_fmt_t",
"rss_fmt_d",
"shr_who",
"sort",
"tail_fd",
"tail_rate",
"tail_tmax",
"tail_who",
"tcolor",
"th_qv",
"th_spec_p",
"txt_eol",
"unlist",
"u2abort",
"u2ts",
"uid",
"gid",
"unp_who",
"ups_who",
"zip_who",
"zipmaxn",
"zipmaxs",
"zipmaxt",
):
ret[k] = k
return ret
@@ -148,25 +210,42 @@ 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",
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
"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",
"fsnt=auto": "filesystem filename traits (lin/win/mac/auto)",
"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": {
"apnd_who=dw": "who can append? (aw/dw/w/no)",
"maxn=250,600": "max 250 uploads over 15min",
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
"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",
@@ -175,6 +254,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": {
@@ -193,10 +273,14 @@ flagcats = {
"d2v": "disables file verification, overrides -e2v*",
"d2d": "disables all database stuff, overrides -e2*",
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
"dbpath=/tmp/cdb": "puts indexes at that location",
"landmark=foo": "disable db if file foo doesn't exist",
"scan=60": "scan for new files every 60sec, same as --re-maxage",
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
"noforget": "don't forget files when deleted from disk",
"forget_ip=43200": "forget uploader-IP after 30 days (GDPR)",
"no_db_ip": "never store uploader-IP in the db; disables unpost",
"fat32": "avoid excessive reindexing on android sdcardfs",
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"xlink": "cross-volume dupe detection / linking (dangerous)",
@@ -205,6 +289,7 @@ flagcats = {
"dotsrch": "show dotfiles in search results",
"nodotsrch": "hide dotfiles in search results (default)",
"srch_excl": "exclude search results with URL matching this regex",
"db_xattr=user.foo,user.bar": "index file xattrs as media-tags",
},
'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': {
"mte=artist,title": "media-tags to index/display",
@@ -221,7 +306,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",
"th_qv=40": "thumbnail quality (10~90)",
"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)": {
@@ -247,12 +335,22 @@ flagcats = {
"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",
"prologues=.prologue.html": "files to embed above/before files",
"epilogues=.epilogue.html": "files to embed below/after files",
"readmes=readme.md,README.md": "files to embed as readmes",
"preadmes=preadme.md,PREADME.md": "files to embed as preadmes",
"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)",
@@ -262,6 +360,15 @@ 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)",
@@ -277,21 +384,53 @@ flagcats = {
"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)",
},
"rss": {
"rss": "allow '?rss' URL suffix (experimental)",
"rss_sort=m": "default sort-order (m/u/n/s)",
"rss_fmt_t={fname}": "default title-format",
"rss_fmt_d={album},{.tn}": "default description-format",
},
"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',
"rss": "allow '?rss' URL suffix (experimental)",
"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',
"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)",
"cachectl=no-cache": "controls caching in webbrowsers",
"mv_retry": "ms-windows: timeout for renaming busy files",
"rm_retry": "ms-windows: timeout for deleting busy files",
"nospawn": "don't create volume's folder if not exist",
"assert_root": "crash on startup if volume's folder not exist",
"davauth": "ask webdav clients to login for all folders",
"davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
},
+3
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
+122 -29
View File
@@ -2,14 +2,15 @@
from __future__ import print_function, unicode_literals
import argparse
import json
import os
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
from .util import chkcmd, json_hesc, min_ex, undot
if True: # pylint: disable=using-constant-test
from typing import Optional, Union
@@ -18,22 +19,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 +45,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 +55,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 +64,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 +83,76 @@ 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")
try:
fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"]
if not fuses or MACOS:
raise Exception()
try:
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6
except:
so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINTS"]) # future
for ln in so.split("\n"):
zsl = ln.split(" ", 1)
if len(zsl) != 2:
continue
fs, mp = zsl
if mp in fuses:
dtab[mp] = fs
except:
pass
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 +169,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 +184,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 +193,73 @@ 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, ""
_fstab: Optional[Fstab] = None
winfs = set(("msdos", "vfat", "ntfs", "exfat"))
# "msdos" = vfat on macos
def ramdisk_chk(asrv: AuthSrv) -> None:
# should have been in authsrv but that's a circular import
global _fstab
mods = []
ramfs = ("tmpfs", "overlay")
log = asrv.log_func or print
if not _fstab:
_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] = tuple(zsl) # type: ignore
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)
assume = "mac" if MACOS else "lin"
for vol in asrv.vfs.all_nodes.values():
if not vol.realpath or vol.flags.get("is_file"):
continue
zs = vol.flags["fsnt"].strip()[:3].lower()
if ANYWIN and not zs:
zs = "win"
if zs in ("lin", "win", "mac"):
vol.flags["fsnt"] = zs
continue
fs = _fstab.get(vol.realpath)[0]
fs = "win" if fs in winfs else assume
htm = json.loads(vol.js_htm)
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
vol.js_htm = json_hesc(json.dumps(htm))
+109 -56
View File
@@ -30,6 +30,7 @@ from .util import (
relchk,
runhook,
sanitize_fn,
set_fperms,
vjoin,
wunlink,
)
@@ -66,13 +67,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 +82,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 +95,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 +151,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(".")
@@ -167,9 +173,19 @@ class FtpFs(AbstractedFS):
t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1)
fn = sanitize_fn(fn or "", "")
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 vfs.flags["emb_all"]
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 +197,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 +216,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 +237,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:
@@ -229,9 +248,36 @@ class FtpFs(AbstractedFS):
need_unlink = False
td = 0
if w and need_unlink:
xbu = vfs.flags.get("xbu")
if xbu:
hr = runhook(
self.log,
None,
self.hub.up2k,
"xbu.ftp",
xbu,
ap,
filename,
"",
"",
"",
0,
0,
"1.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" % (filename,)
self.log(t, 3)
raise FSE(t)
if w and need_unlink: # type: ignore # !rm
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 +294,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 +325,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 +347,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 +395,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 +425,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 +514,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)
@@ -587,11 +640,11 @@ class Ftpd(object):
lgr = logging.getLogger("pyftpdlib")
lgr.setLevel(logging.DEBUG if self.args.ftpv else logging.INFO)
ips = self.args.i
ips = self.args.ftp_i
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]
+1759 -640
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -62,6 +62,7 @@ class HttpConn(object):
self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu
self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm
self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm
self.ipar_nm: Optional[NetMap] = hsrv.ipar_nm
self.xff_nm: Optional[NetMap] = hsrv.xff_nm
self.xff_lan: NetMap = hsrv.xff_lan # type: ignore
self.iphash: HMaccas = hsrv.broker.iphash
@@ -224,3 +225,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()
+30 -4
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,7 +195,13 @@ 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.ipar_nm = build_netmap(self.args.ipar)
self.xff_nm = build_netmap(self.args.xff_src)
self.xff_lan = build_netmap("lan")
@@ -313,6 +323,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 +332,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 +384,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 +407,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 +573,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)
+13 -2
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")
+68 -26
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:
set_avahi_379()
if not self.args.zm_nwa_1 and DNS_VND:
set_avahi_379() # type: ignore
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
+2 -2
View File
@@ -17,10 +17,10 @@ class Metrics(object):
self.hsrv = hsrv
def tx(self, cli: "HttpCli") -> bool:
if not cli.avol:
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)
args = cli.args
if not args.stats:
raise Pebkac(403, "the stats feature is not enabled in server config")
+154 -18
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 # type: ignore # 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):
@@ -371,6 +465,8 @@ class MTag(object):
"ffprobe" if args.no_mutagen or (HAVE_FFPROBE and EXE) else "mutagen"
)
self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff
self.read_xattrs = args.have_db_xattr
self.get = self._get_xattr if self.read_xattrs else self._get_main
mappings = args.mtm
or_ffprobe = " or FFprobe"
@@ -392,7 +488,13 @@ class MTag(object):
msg = "found FFprobe but it was disabled by --no-mtag-ff"
self.log(msg, c=3)
if self.read_xattrs and not self.usable:
t = "don't have the necessary dependencies to read conventional media tags, but will read xattrs"
self.log(t)
self.usable = True
if not self.usable:
self._get = None
if EXE:
t = "copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: "
self.log(t + FFMPEG_URL)
@@ -424,13 +526,12 @@ class MTag(object):
"album-artist",
"tpe2",
"aart",
"conductor",
"organization",
"band",
],
".tn": ["tracknumber", "trck", "trkn", "track"],
"genre": ["genre", "tcon", "\u00a9gen"],
"date": [
"tdate": [
"original-release-date",
"release-date",
"date",
@@ -552,12 +653,44 @@ class MTag(object):
return r1
def get(self, abspath: str) -> dict[str, Union[str, float]]:
def _get_xattr(
self, abspath: str, vf: dict[str, Any]
) -> dict[str, Union[str, float]]:
ret = self._get_main(abspath, vf) if self._get else {}
if "db_xattr_no" in vf:
try:
neg = vf["db_xattr_no"]
zsl = os.listxattr(abspath)
zsl = [x for x in zsl if x not in neg]
for xattr in zsl:
zb = os.getxattr(abspath, xattr)
ret[xattr] = zb.decode("utf-8", "replace")
except:
self.log("failed to read xattrs from [%s]\n%s", abspath, min_ex(), 3)
elif "db_xattr_yes" in vf:
for xattr in vf["db_xattr_yes"]:
if "=" in xattr:
xattr, name = xattr.split("=", 1)
else:
name = xattr
try:
zs = os.getxattr(abspath, xattr)
ret[name] = zs.decode("utf-8", "replace")
except:
pass
return ret
def _get_main(
self, abspath: str, vf: dict[str, Any]
) -> dict[str, Union[str, float]]:
ext = abspath.split(".")[-1].lower()
if ext not in self.args.au_unpk:
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 +762,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 +796,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]
+7 -8
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()
@@ -183,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:
+6 -1
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
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))
+869
View File
@@ -0,0 +1,869 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import errno
import hashlib
import logging
import os
import select
import socket
import time
from threading import ExceptHookArgs
import paramiko
import paramiko.common
import paramiko.sftp_attr
from paramiko.common import AUTH_FAILED, AUTH_SUCCESSFUL
from paramiko.sftp import (
SFTP_FAILURE,
SFTP_NO_SUCH_FILE,
SFTP_OK,
SFTP_OP_UNSUPPORTED,
SFTP_PERMISSION_DENIED,
)
from .__init__ import ANYWIN, TYPE_CHECKING
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
from .bos import bos
from .util import (
VF_CAREFUL,
Daemon,
ODict,
Pebkac,
ipnorm,
min_ex,
read_utf8,
relchk,
runhook,
sanitize_fn,
ub64enc,
undot,
vjoin,
wunlink,
)
if TYPE_CHECKING:
from .svchub import SvcHub
if True: # pylint: disable=using-constant-test
from typing import Any, BinaryIO, Optional, Union
SATTR = paramiko.sftp_attr.SFTPAttributes
class SSH_Srv(paramiko.ServerInterface):
def __init__(self, hub: "SvcHub", addr: Any):
self.hub = hub
self.args = args = hub.args
self.log_func = hub.log
self.uname = "*"
self.addr = addr
self.ip = addr[0]
if self.ip.startswith("::ffff:"):
self.ip = self.ip[7:]
zsl = []
if args.sftp_anon:
zsl.append("none")
if args.sftp_key2u:
zsl.append("publickey")
if args.sftp_pw or args.sftp_anon:
zsl.append("password")
self._auths = ",".join(zsl)
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log("sftp:%s" % (self.ip,), msg, c)
def get_allowed_auths(self, username: str) -> str:
return self._auths
def get_banner(self) -> tuple[Optional[str], Optional[str]]:
if self.args.sftpv:
self.log("get_banner")
t = self.args.sftp_banner
if not t:
return (None, None)
if t.startswith("@"):
t = read_utf8(self.log, t[1:], False)
if t and not t.endswith("\n"):
t += "\n"
return (t, "en-US")
def check_channel_request(self, kind: str, chanid: int) -> int:
if self.args.sftpv:
self.log("channel-request: %r, %r" % (kind, chanid))
if kind == "session":
return paramiko.common.OPEN_SUCCEEDED
return paramiko.common.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_auth_none(self, username: str) -> int:
try:
return self._check_auth_none(username)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return AUTH_FAILED
def _check_auth_none(self, uname: str) -> int:
args = self.args
if uname != args.sftp_anon or not uname:
return AUTH_FAILED
ipn = ipnorm(self.ip)
bans = self.hub.bans
if ipn in bans:
rt = bans[ipn] - time.time()
if rt < 0:
self.log("client unbanned")
del bans[ipn]
else:
self.log("client is banned")
return AUTH_FAILED
self.uname = "*"
self.log("auth-none OK: *")
return AUTH_SUCCESSFUL
def check_auth_password(self, username: str, password: str) -> int:
try:
return self._check_auth_password(username, password)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return AUTH_FAILED
def _check_auth_password(self, uname: str, pw: str) -> int:
args = self.args
if args.sftpv:
logpw = pw
if args.log_badpwd == 0:
logpw = ""
elif args.log_badpwd == 2:
zb = hashlib.sha512(pw.encode("utf-8", "replace")).digest()
logpw = "%" + ub64enc(zb[:12]).decode("ascii")
self.log("auth-pw: %r, %r" % (uname, logpw))
ipn = ipnorm(self.ip)
bans = self.hub.bans
if ipn in bans:
rt = bans[ipn] - time.time()
if rt < 0:
self.log("client unbanned")
del bans[ipn]
else:
self.log("client is banned")
return AUTH_FAILED
anon = args.sftp_anon
if anon and uname == anon:
self.uname = "*"
self.log("auth-pw OK: *")
return AUTH_SUCCESSFUL
if not args.sftp_pw:
return AUTH_FAILED
if args.usernames:
alts = ["%s:%s" % (uname, pw)]
else:
alts = [pw, uname]
attempt = "%s:%s" % (uname, pw)
uname = ""
asrv = self.hub.asrv
for zs in alts:
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs:
uname = zs
break
if args.ipu and uname == "*":
uname = args.ipu_iu[args.ipu_nm.map(self.ip)]
if args.ipr and uname in args.ipr_u:
if not args.ipr_u[uname].map(self.ip):
logging.warning("username [%s] rejected by --ipr", uname)
return AUTH_FAILED
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd
if g.lim:
bonk, ip = g.bonk(self.ip, attempt)
if bonk:
logging.warning("client banned: invalid passwords")
bans[self.ip] = bonk
try:
# only possible if multiprocessing disabled
self.hub.broker.httpsrv.bans[ip] = bonk # type: ignore
self.hub.broker.httpsrv.nban += 1 # type: ignore
except:
pass
return AUTH_FAILED
self.uname = uname
self.log("auth-pw OK: %s" % (uname,))
return AUTH_SUCCESSFUL
def check_auth_publickey(self, username: str, key: paramiko.PKey) -> int:
try:
return self._check_auth_publickey(username, key)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return AUTH_FAILED
def _check_auth_publickey(self, uname: str, key: paramiko.PKey) -> int:
args = self.args
if args.sftpv:
zs = key.get_name() + "," + key.get_base64()[:32]
self.log("auth-key: %r, %r" % (uname, zs))
ipn = ipnorm(self.ip)
bans = self.hub.bans
if ipn in bans:
rt = bans[ipn] - time.time()
if rt < 0:
self.log("client unbanned")
del bans[ipn]
else:
self.log("client is banned")
return AUTH_FAILED
anon = args.sftp_anon
if anon and uname == anon:
self.uname = "*"
self.log("auth-key OK: *")
return AUTH_SUCCESSFUL
attempt = "%s %s" % (key.get_name(), key.get_base64())
ok = args.sftp_key2u.get(attempt) == uname
if ok and args.ipr and uname in args.ipr_u:
if not args.ipr_u[uname].map(self.ip):
logging.warning("username [%s] rejected by --ipr", uname)
return AUTH_FAILED
asrv = self.hub.asrv
if not ok or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
self.log("auth-key REJECTED: %s" % (uname,))
return AUTH_FAILED
self.uname = uname
self.log("auth-key OK: %s" % (uname,))
return AUTH_SUCCESSFUL
class SFTP_FH(paramiko.SFTPHandle):
def __init__(self, flags: int = 0) -> None:
self.filename = ""
self.readfile: Optional[BinaryIO] = None
self.writefile: Optional[BinaryIO] = None
super(SFTP_FH, self).__init__(flags)
def stat(self):
try:
f = self.readfile or self.writefile
return SATTR.from_stat(os.fstat(f.fileno()))
except OSError as ex:
return paramiko.SFTPServer.convert_errno(ex.errno)
def chattr(self, attr):
# python doesn't have equivalents to fchown or fchmod, so we have to
# use the stored filename
if not self.writefile:
return SFTP_PERMISSION_DENIED
try:
paramiko.SFTPServer.set_file_attr(self.filename, attr)
return SFTP_OK
except OSError as ex:
return paramiko.SFTPServer.convert_errno(ex.errno)
class SFTP_Srv(paramiko.SFTPServerInterface):
def __init__(self, ssh: paramiko.ServerInterface, *a, **ka):
super(SFTP_Srv, self).__init__(ssh, *a, **ka)
self.ssh = ssh
self.ip: str = ssh.ip # type: ignore
self.hub: "SvcHub" = ssh.hub # type: ignore
self.uname: str = ssh.uname # type: ignore
self.args = self.hub.args
self.asrv: "AuthSrv" = self.hub.asrv
self.v = self.args.sftpv
self.vv = self.args.sftpvv
if self.uname == LEELOO_DALLAS:
raise Exception("send her back")
self.vols = [
vp
for vp, vn in self.asrv.vfs.all_vols.items()
if self.uname in vn.axs.uread
or self.uname in vn.axs.uwrite
or self.uname in vn.axs.uget
]
self.vis = set()
for zs in self.vols:
self.vis.add(zs)
while zs:
zs = zs.rsplit("/", 1)[0] if "/" in zs else ""
self.vis.add(zs)
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log("sftp:%s" % (self.ip,), msg, c)
def v2a(
self,
vpath: str,
r: bool = False,
w: bool = False,
m: bool = False,
d: bool = False,
) -> tuple[str, VFS, str]:
vpath = vpath.replace(os.sep, "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
self.log("malicious vpath: %s", vpath)
raise Exception("Unsupported characters in [%s]" % (vpath,))
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if (
w
and fn.lower() in vn.flags["emb_all"]
and self.uname not in vn.axs.uread
and "wo_up_readme" not in vn.flags
):
fn = "_wo_" + fn
vpath = vjoin(rd, fn)
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vn.realpath:
# return "", vn, rem
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
if "xdev" in vn.flags or "xvol" in vn.flags:
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
t = "Permission denied in [{}]"
if not avn:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
return os.path.join(vn.realpath, rem), vn, rem
def list_folder(self, path: str) -> list[SATTR] | int:
try:
return self._list_folder(path)
except Pebkac as ex:
if ex.code == 404:
self.log("folder 404: %s" % (path,))
return SFTP_NO_SUCH_FILE
return SFTP_PERMISSION_DENIED
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _list_folder(self, path: str) -> list[SATTR] | int:
if self.v:
self.log("ls(%s):" % (path,))
path = path.strip("/")
try:
ap, vn, rem = self.v2a(path, r=True)
except Pebkac:
try:
self.v2a(path, w=True)
self.log("ls(%s): [] (write-only)" % (path,))
return [] # display write-only folders as empty
except:
pass
if path not in self.vis:
self.log("ls(%s): EPERM" % (path,))
return SFTP_PERMISSION_DENIED
# list of accessible volumes
ret = []
zi = int(time.time())
vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
prefix = path + "/"
for vn in self.asrv.vfs.all_nodes.values():
if path and not vn.vpath.startswith(prefix):
continue # vn is parent
vname = vn.vpath[len(prefix) :]
if "/" in vname or not vname:
continue # only include vols at current level
ret.append(SATTR.from_stat(vst, filename=vn.vpath))
ret.sort(key=lambda x: x.filename)
self.log("ls(%s): vfs-vols; |%d|" % (path, len(ret)))
return ret
_, vfs_ls, vfs_virt = vn.ls(
rem,
self.uname,
not self.args.no_scandir,
[[True, False], [False, True]],
throw=True,
)
ret = [SATTR.from_stat(x[1], filename=x[0]) for x in vfs_ls]
for zs, vn2 in vfs_virt.items():
if not vn2.realpath:
continue
st = bos.stat(vn2.realpath)
ret.append(SATTR.from_stat(st, filename=zs))
if self.uname not in vn.axs.udot:
ret = [x for x in ret if not x.filename.split("/")[-1].startswith(".")]
ret.sort(key=lambda x: x.filename)
self.log("ls(%s): |%d|" % (path, len(ret)))
return ret
def stat(self, path: str) -> SATTR | int:
try:
return self._stat(path)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def lstat(self, path: str) -> SATTR | int:
try:
return self._stat(path)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _stat(self, vp: str) -> SATTR | int:
vp = vp.strip("/")
try:
ap, vn, _ = self.v2a(vp)
if (
self.uname not in vn.axs.uread
and self.uname not in vn.axs.uwrite
and self.uname not in vn.axs.uget
):
self.log("stat(%s): EPERM" % (vp,))
return SFTP_PERMISSION_DENIED
st = bos.stat(ap)
self.log("stat(%s): %s" % (vp, st))
except:
if vp not in self.vis:
self.log("stat(%s): ENOENT" % (vp,))
return SFTP_NO_SUCH_FILE
zi = int(time.time())
st = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
self.log("stat(%s): vfs-vols")
return SATTR.from_stat(st)
def open(self, path: str, flags: int, attr: SATTR) -> paramiko.SFTPHandle | int:
try:
return self._open(path, flags, attr)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _open(self, vp: str, iflag: int, attr: SATTR) -> paramiko.SFTPHandle | int:
if ANYWIN:
iflag |= os.O_BINARY
if iflag & os.O_WRONLY:
rd = False
wr = True
if iflag & os.O_APPEND:
smode = "ab"
else:
smode = "wb"
elif iflag & os.O_RDWR:
rd = wr = True
if iflag & os.O_APPEND:
smode = "a+b"
else:
smode = "r+b"
else:
rd = True
wr = False
smode = "rb"
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr)
ap = os.path.join(vn.realpath, rem)
vf = vn.flags
except Pebkac as ex:
t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s"
self.log(t % (vp, iflag, rd, wr, ex))
return SFTP_PERMISSION_DENIED
self.log("open(%s, %x, %s)" % (vp, iflag, smode))
if wr:
try:
st = bos.stat(ap)
td = time.time() - st.st_mtime
need_unlink = True
except:
need_unlink = False
td = 0
xbu = vn.flags.get("xbu")
if xbu:
hr = runhook(
self.log,
None,
self.hub.up2k,
"xbu.sftp",
xbu,
ap,
vp,
"",
"",
"",
0,
0,
"7.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" % (vp,)
self.log(t, 3)
return SFTP_PERMISSION_DENIED
self.log("writing to [%s] => [%s]" % (vp, ap))
if wr and need_unlink: # type: ignore # !rm
assert td # type: ignore # !rm
if td >= -1 and td <= self.args.ftp_wt:
# 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
do_it = False
else:
# allow overwrite if user has delete permission
do_it = self.uname in vn.axs.udel
if not do_it:
t = "file already exists and no permission to overwrite: %s"
self.log(t % (vp,))
return SFTP_PERMISSION_DENIED
# Don't unlink file for append mode
elif "a" not in smode:
wunlink(self.log, ap, VF_CAREFUL)
chmod = getattr(attr, "st_mode", None)
if chmod is None:
chmod = vf.get("chmod_f", 0o644)
self.log("open(%s, %x): client did not chmod" % (vp, iflag))
else:
self.log("open(%s, %x): client set chmod 0%o" % (vp, iflag, chmod))
try:
fd = os.open(ap, iflag, chmod)
except OSError as ex:
t = "failed to os.open [%s] -> [%s] with iflag [%s] and chmod [%s]: %r"
self.log(t % (vp, ap, iflag, chmod, ex), 3)
return paramiko.SFTPServer.convert_errno(ex.errno)
if iflag & os.O_CREAT:
paramiko.SFTPServer.set_file_attr(ap, attr)
try:
f = os.fdopen(fd, smode)
except OSError as ex:
t = "failed to os.fdpen [%s] -> [%s] with smode [%s]: %r"
self.log(t % (vp, ap, smode, ex), 3)
return paramiko.SFTPServer.convert_errno(ex.errno)
ret = SFTP_FH(iflag)
ret.filename = ap
ret.readfile = f if rd else None
ret.writefile = f if wr else None
return ret
def remove(self, path: str) -> int:
try:
return self._remove(path)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _remove(self, vp: str) -> int:
self.log("rm(%s)" % (vp,))
if self.args.no_del:
self.log("The delete feature is disabled in server config")
return SFTP_PERMISSION_DENIED
try:
self.hub.up2k.handle_rm(self.uname, self.ip, [vp], [], False, False)
self.log("rm(%s): ok" % (vp,))
return SFTP_OK
except Pebkac as ex:
t = "denied delete [%s]: %s"
self.log(t % (vp, ex))
if str(ex).startswith("file not found"):
return SFTP_NO_SUCH_FILE
try:
# write-only client trying to rm before upload?
ap, vn, _ = self.v2a(vp)
if (
self.uname not in vn.axs.uread
and self.uname not in vn.axs.uwrite
and self.uname not in vn.axs.uget
):
self.log("rm(%s): EPERM" % (vp,))
return SFTP_PERMISSION_DENIED
if not bos.path.exists(ap):
self.log(" `- file didn't exist; returning ENOENT")
return SFTP_NO_SUCH_FILE
except:
pass
return SFTP_PERMISSION_DENIED
except OSError as ex:
self.log("failed: rm(%s): %r" % (vp, ex))
return paramiko.SFTPServer.convert_errno(ex.errno)
def rename(self, oldpath: str, newpath: str) -> int:
try:
return self._rename(oldpath, newpath)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _rename(self, svp: str, dvp: str) -> int:
self.log("mv(%s, %s)" % (svp, dvp))
if self.args.no_mv:
self.log("The rename/move feature is disabled in server config")
svp = svp.strip("/")
dvp = dvp.strip("/")
try:
self.hub.up2k.handle_mv("", self.uname, self.ip, svp, dvp)
return SFTP_OK
except Pebkac as ex:
t = "denied rename [%s] to [%s]: %s"
self.log(t % (svp, dvp, ex))
return SFTP_PERMISSION_DENIED
except OSError as ex:
self.log("mv(%s, %s): %r" % (svp, dvp, ex))
return paramiko.SFTPServer.convert_errno(ex.errno)
def mkdir(self, path: str, attr: SATTR) -> int:
try:
return self._mkdir(path, attr)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _mkdir(self, vp: str, attr: SATTR) -> int:
self.log("mkdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
ap = os.path.join(vn.realpath, rem)
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
if attr is not None:
paramiko.SFTPServer.set_file_attr(ap, attr)
return SFTP_OK
except Pebkac as ex:
t = "denied mkdir [%s]: %s"
self.log(t % (vp, ex))
return SFTP_PERMISSION_DENIED
except OSError as ex:
self.log("mkdir(%s): %r" % (vp, ex))
return paramiko.SFTPServer.convert_errno(ex.errno)
def rmdir(self, path: str) -> int:
try:
return self._rmdir(path)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _rmdir(self, vp: str) -> int:
self.log("rmdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
ap = os.path.join(vn.realpath, rem)
bos.rmdir(ap)
return SFTP_OK
except Pebkac as ex:
t = "denied rmdir [%s]: %s"
self.log(t % (vp, ex))
return SFTP_PERMISSION_DENIED
except OSError as ex:
self.log("rmdir(%s): %r" % (vp, ex))
return paramiko.SFTPServer.convert_errno(ex.errno)
def chattr(self, path: str, attr: SATTR) -> int:
try:
return self._chattr(path, attr)
except:
self.log("unhandled exception: %s" % (min_ex(),), 1)
return SFTP_FAILURE
def _chattr(self, vp: str, attr: SATTR) -> int:
self.log("chattr(%s, %s)" % (vp, attr))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
ap = os.path.join(vn.realpath, rem)
paramiko.SFTPServer.set_file_attr(ap, attr)
return SFTP_OK
except Pebkac as ex:
t = "denied chattr [%s]: %s"
self.log(t % (vp, ex))
return SFTP_PERMISSION_DENIED
except OSError as ex:
self.log("chattr(%s): %r" % (vp, ex))
return paramiko.SFTPServer.convert_errno(ex.errno)
def symlink(self, target_path: str, path: str) -> int:
return SFTP_OP_UNSUPPORTED
def readlink(self, path: str) -> str | int:
return path
def canonicalize(self, path: str) -> str:
return "/%s" % (undot(path),)
class Sftpd(object):
def __init__(self, hub: "SvcHub") -> None:
self.hub = hub
self.args = args = hub.args
self.log_func = hub.log
self.srv: list[socket.socket] = []
self.bound: list[str] = []
self.sessions = {}
ips = args.sftp_i
if "::" in ips:
ips.append("0.0.0.0")
ips = [x for x in ips if not x.startswith(("unix:", "fd:"))]
if args.sftp4:
ips = [x for x in ips if ":" not in x]
if not ips:
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
return
self.hostkeys = []
hostkeytypes = (
("ed25519", "Ed25519Key", {}), # best
("ecdsa", "ECDSAKey", {"bits": 384}),
("rsa", "RSAKey", {"bits": 4096}),
("dsa", "DSSKey", {}), # worst
)
for fname, aname, opts in hostkeytypes:
fpath = "%s/ssh_host_%s_key" % (args.sftp_hostk, fname.lower())
try:
pkey = getattr(paramiko, aname).from_private_key_file(fpath)
except Exception as ex:
try:
genfun = getattr(paramiko, aname).generate
except Exception as ex2:
if args.sftpv or fname not in ("dsa", "ed25519"):
# dsa dropped in 4.0
# ed25519 not supported yet
self.log("cannot generate %s hostkey: %r" % (aname, ex2), 3)
continue
self.log("generating hostkey [%s] due to %r" % (fpath, ex))
pkey = genfun(**opts)
pkey.write_private_key_file(fpath)
pkey = getattr(paramiko, aname).from_private_key_file(fpath)
self.hostkeys.append(pkey)
if args.sftpv:
self.log("loaded hostkey %r" % (pkey,))
ips = list(ODict.fromkeys(ips)) # dedup
for ip in ips:
self._bind(ip)
self.log("listening @ %s port %s" % (self.bound, args.sftp))
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.hub.log("sftp", msg, c)
def _bind(self, ip: str) -> None:
port = self.args.sftp
try:
ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET
srv = socket.socket(ipv, socket.SOCK_STREAM)
if not ANYWIN or self.args.reuseaddr:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
srv.settimeout(0) # == srv.setblocking(False)
try:
srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
except:
pass # will create another ipv4 socket instead
if getattr(self.args, "freebind", False):
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
srv.bind((ip, port))
srv.listen(10)
self.srv.append(srv)
self.bound.append(ip)
except Exception as ex:
if ip == "0.0.0.0" and "::" in self.bound:
return # dualstack
self.log("could not listen on (%s,%s): %r" % (ip, port, ex), 3)
def _accept(self, srv: socket.socket) -> None:
cli, addr = srv.accept()
# cli.settimeout(0) # == srv.setblocking(False)
self.log("%r is connecting" % (addr,))
zs = "sftp-%s" % (addr[0],)
# Daemon(self._accept2, zs, (cli, addr))
self._accept2(cli, addr)
def _accept2(self, cli, addr) -> None:
tra = paramiko.Transport(cli)
for hkey in self.hostkeys:
tra.add_server_key(hkey)
tra.set_subsystem_handler("sftp", paramiko.SFTPServer, SFTP_Srv)
psrv = SSH_Srv(self.hub, addr)
try:
tra.start_server(server=psrv)
except Exception as ex:
self.log("%r could not establish connection: %r" % (addr, ex), 3)
cli.close()
return
chan = tra.accept()
if chan is None:
self.log("%r did not open an sftp channel" % (addr,), 3)
cli.close()
return
self.sessions[addr] = (chan, tra, psrv)
# tra.join()
# self.log("%r disconnected" % (addr,))
def run(self):
lgr = logging.getLogger("paramiko.transport")
lgr.setLevel(logging.DEBUG if self.args.sftpvv else logging.INFO)
if self.args.no_poll:
fun = self._run_select
else:
fun = self._run_poll
Daemon(fun, "sftpd")
def _run_select(self):
while not self.hub.stopping:
rx, _, _ = select.select(self.srv, [], [], 180)
for sck in rx:
self._accept(sck)
def _run_poll(self):
fd2sck = {}
poll = select.poll()
for sck in self.srv:
fd = sck.fileno()
fd2sck[fd] = sck
poll.register(fd, select.POLLIN)
while not self.hub.stopping:
pr = poll.poll(180 * 1000)
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
for sck in rx:
self._accept(sck)
+28 -23
View File
@@ -89,7 +89,7 @@ class SMB(object):
smbserver.isInFileJail = self._is_in_file_jail
self._disarm()
ip = next((x for x in self.args.i if ":" not in x), None)
ip = next((x for x in self.args.smb_i if ":" not in x), None)
if not ip:
self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3)
ip = "0.0.0.0"
@@ -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 = "?"
+1 -45
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))
+4 -3
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])
+552 -114
View File
File diff suppressed because it is too large. Load diff
+12 -9
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 VPTL_WIN, min_ex, sanitize_to, 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
@@ -105,7 +104,7 @@ def gen_hdr(
ret += spack(b"<LL", vsz, vsz)
# windows support (the "?" replace below too)
fn = sanitize_fn(fn, "/")
fn = sanitize_to(fn, VPTL_WIN)
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
@@ -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
+94 -18
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()
@@ -382,6 +422,7 @@ class TcpSrv(object):
self.hub.broker.say("httpsrv.set_netdevs", self.netdevs)
self.hub.start_zeroconf()
gencert(self.log, self.args, self.netdevs)
self.hub.restart_sftpd()
self.hub.restart_ftpd()
self.hub.restart_tftpd()
@@ -417,9 +458,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 +587,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 +604,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 +616,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 +624,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 +643,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 +661,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 +670,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
+64 -31
View File
@@ -36,7 +36,19 @@ 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 (
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
@@ -162,11 +174,11 @@ class Tftpd(object):
p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")]
ports = list(range(p1, p2 + 1))
ips = self.args.i
ips = self.args.tftp_i
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 +256,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 vfs.flags["emb_all"]:
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 +284,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 +297,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 +353,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 +362,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 +392,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 +433,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 +441,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:
+29 -6
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
+442 -73
View File
@@ -2,32 +2,36 @@
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
from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .authsrv import VFS
from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
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,61 @@ 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}$")
# for n in {1..100}; do rm -rf /home/ed/Pictures/wp/.hist/th/ ; python3 -m copyparty -qv /home/ed/Pictures/wp/::r --th-no-webp --th-qv $n --th-dec pil >/dev/null 2>&1 & p=$!; printf '\033[A\033[J%3d ' $n; while true; do sleep 0.1; curl -s 127.1:3923 >/dev/null && break; done; curl -s '127.1:3923/?tar=j' >/dev/null ; cat /home/ed/Pictures/wp/.hist/th/1n/bs/1nBsjDetfie1iDq3y2D4YzF5/*.* | wc -c; kill $p; wait >/dev/null 2>&1; done
# filesize-equivalent, not quality (ff looks much shittier)
FF_JPG_Q = {
0: b"30", # 0
1: b"30", # 5
2: b"30", # 10
3: b"30", # 15
4: b"28", # 20
5: b"21", # 25
6: b"17", # 30
7: b"15", # 35
8: b"13", # 40
9: b"12", # 45
10: b"11", # 50
11: b"10", # 55
12: b"9", # 60
13: b"8", # 65
14: b"7", # 70
15: b"6", # 75
16: b"5", # 80
17: b"4", # 85
18: b"3", # 90
19: b"2", # 95
20: b"2", # 100
}
# FF_JPG_Q = {xn: ("%d" % (xn,)).encode("ascii") for xn in range(2, 33)}
VIPS_JPG_Q = {
0: 4, # 0
1: 7, # 5
2: 12, # 10
3: 17, # 15
4: 22, # 20
5: 27, # 25
6: 32, # 30
7: 37, # 35
8: 42, # 40
9: 47, # 45
10: 52, # 50
11: 56, # 55
12: 61, # 60
13: 66, # 65
14: 71, # 70
15: 75, # 75
16: 80, # 80
17: 85, # 85
18: 89, # 90 (vips explodes past this point)
19: 91, # 95
20: 97, # 100
}
try:
if os.environ.get("PRTY_NO_PIL"):
@@ -78,7 +136,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 +150,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 +166,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 +242,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 +273,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 +308,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 +320,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,12 +351,14 @@ 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):
with open(inf_path, "wb") as f:
f.write(afsenc(os.path.dirname(abspath)))
self.writevolcfg(histpath)
self.busy[tpath] = [cond]
do_conv = True
@@ -270,7 +368,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,11 +396,53 @@ 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,
}
def volcfgi(self, vn: VFS) -> str:
ret = []
zs = "th_dec th_no_webp th_no_jpg"
for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, getattr(self.args, zs)))
zs = "th_qv thsize th_spec_p convt"
for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, vn.flags.get(zs)))
return "".join(ret)
def volcfga(self, vn: VFS) -> str:
ret = []
zs = "q_opus q_mp3"
for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, getattr(self.args, zs)))
zs = "aconvt"
for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, vn.flags.get(zs)))
return "".join(ret)
def writevolcfg(self, histpath: str) -> None:
try:
bos.stat(os.path.join(histpath, "th", "cfg.txt"))
bos.stat(os.path.join(histpath, "ac", "cfg.txt"))
return
except:
pass
cfgi = cfga = ""
for vn in self.asrv.vfs.all_vols.values():
if vn.histpath == histpath:
cfgi = self.volcfgi(vn)
cfga = self.volcfga(vn)
break
t = "writing thumbnailer-config %d,%d to %s"
self.log(t % (len(cfgi), len(cfga), histpath))
chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755
for cfg, cat in ((cfgi, "th"), (cfga, "ac")):
bos.makedirs(os.path.join(histpath, cat), vf=chmod)
with open(os.path.join(histpath, cat, "cfg.txt"), "wb") as f:
f.write(cfg.encode("utf-8"))
def wait4ram(self, need: float, ttpath: str) -> None:
ram = self.args.th_ram_max
if need > ram * 0.99:
@@ -336,12 +476,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 +493,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 +519,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 +535,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 +548,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 +613,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": vn.flags["th_qv"]}
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)
@@ -493,11 +665,65 @@ class ThumbSrv(object):
raise
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
args = {}
qv = vn.flags["th_qv"]
if tpath.endswith("jpg"):
qv = VIPS_JPG_Q[qv // 5]
args["optimize_coding"] = True
img.write_to_file(tpath, Q=qv, strip=True, **args)
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
args = {}
qv = vn.flags["th_qv"]
if tpath.endswith("jpg"):
qv = VIPS_JPG_Q[qv // 5]
args["optimize_coding"] = True
img.write_to_file(tpath, Q=qv, strip=True, **args)
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 +734,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 +763,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",
@@ -536,27 +773,27 @@ class ThumbSrv(object):
if tpath.endswith(".jpg"):
cmd += [
b"-q:v",
b"6", # default=??
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
else:
cmd += [
b"-q:v",
b"50", # default=75
unicode(vn.flags["th_qv"]).encode("ascii"), # default=75
b"-compression_level:v",
b"6", # default=4, 0=fast, 6=max
]
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")
@@ -587,7 +824,7 @@ class ThumbSrv(object):
if len(lines) > 50:
lines = lines[:25] + ["[...]"] + lines[-25:]
txt = "\n".join(["ff: " + str(x) for x in lines])
txt = "\n".join(["ff: " + unicode(x) for x in lines])
if len(txt) > 5000:
txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:]
@@ -595,7 +832,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 +870,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 +889,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 +972,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",
@@ -697,18 +982,18 @@ class ThumbSrv(object):
if tpath.endswith(".jpg"):
cmd += [
b"-q:v",
b"6", # default=??
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
else:
cmd += [
b"-q:v",
b"50", # default=75
unicode(vn.flags["th_qv"]).encode("ascii"), # default=75
b"-compression_level:v",
b"6", # default=4, 0=fast, 6=max
]
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 +1001,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 +1032,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 +1148,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 +1188,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 +1215,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 +1234,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)
@@ -900,7 +1245,7 @@ class ThumbSrv(object):
ret = []
for k, vs in raw_tags.items():
for v in vs:
if len(str(v)) >= 1024:
if len(unicode(v)) >= 1024:
bv = k.encode("utf-8", "replace")
ret += [b"-metadata", bv + b"="]
break
@@ -938,6 +1283,28 @@ class ThumbSrv(object):
time.sleep(interval)
def clean(self, histpath: str) -> int:
cfgi = cfga = ""
for vn in self.asrv.vfs.all_vols.values():
if vn.histpath == histpath:
cfgi = self.volcfgi(vn)
cfga = self.volcfga(vn)
break
for cfg, cat in ((cfgi, "th"), (cfga, "ac")):
if not cfg:
continue
try:
with open(os.path.join(histpath, cat, "cfg.txt"), "rb") as f:
oldcfg = f.read().decode("utf-8")
except:
oldcfg = ""
if cfg == oldcfg:
continue
zs = os.path.join(histpath, cat)
if not os.path.exists(zs):
continue
self.log("thumbnailer-config changed; deleting %s" % (zs,), 3)
shutil.rmtree(zs)
ret = 0
for cat in ["th", "ac"]:
top = os.path.join(histpath, cat)
@@ -991,10 +1358,12 @@ 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:
if f != "dir.txt":
if f != "dir.txt" and f != "cfg.txt":
self.log("foreign file in thumbs dir: [{}]".format(fp), 1)
continue
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more