Compare commits

...

135 Commits

Author SHA1 Message Date
ed
dccef40f3d v1.18.8 2025-07-31 08:33:34 +00:00
ed
c17ce4892e fix pkgres on older python3 versions 2025-07-31 08:32:52 +00:00
ed
5df2cbe5d7 update pkgs to 1.18.7 2025-07-30 21:59:58 +00:00
ed
daa44be1a5 v1.18.7 2025-07-30 21:31:54 +00:00
ed
13d5631b48 more escapes in case 2025-07-30 21:26:27 +00:00
ed
a8705e611d fix GHSA-8mx2-rjh8-q3jq ;
this fixes a DOM-Based XSS in the recent-uploads page:

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

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

* nix: allow passing extra python packages

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

* nix: format all nix files with nixfmt

* nix: reduce redundancy in the package

For readability

* nix: remove unused pyftpdlib import

* nix: put makeWrapper into the correct inputs

* nix: fill out all of meta

* nix: set formatter in flake for nix files

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

* nix: add u2c

* nix: add partyfuse

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

* nix: add missing cfssl import

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

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

* nix: use correct overlay argument names

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

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

* add youtube link too

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* added ability to specify user and group.

* added option to have hist data live with volumes.

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

* added environment script.

* Revert "added environment script."

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

This reverts commit c60c8d8e0b.

* fixup! added ability to specify user and group.

* Reapply "added environment script."

This reverts commit a54e950ecc.

* Moved back to TemporaryFileSystem for system hardening.

I misunderstood bind mounts...

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

* changed copyparty-env script to copyparty-hash.

* removed seperatehist in favor of default settings attrset.

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

* minor refactoring.

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

* fixup! removed seperatehist in favor of default settings attrset.
2025-04-29 14:48:17 +02:00
ed
94352f278b non-https clipboard newlines; fixes #161 2025-04-28 19:00:13 +00:00
ed
4fb87ebe32 flatcase best case 2025-04-27 09:25:01 +00:00
ed
3cbb7243ab update pkgs to 1.17.0 2025-04-26 22:50:45 +00:00
83 changed files with 3781 additions and 688 deletions

View File

@@ -34,7 +34,7 @@ remove the ones that are not relevant:
### Server details (if you're NOT using docker/podman)
remove the ones that are not relevant:
* **server OS / version:**
* **what copyparty did you grab:** (sfx/exe/pip/aur/...)
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
* **how you're running it:** (in a terminal, as a systemd-service, ...)
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
* **copyparty arguments and/or config-file:**

View File

@@ -8,11 +8,13 @@ turn almost any device into a file server with resumable uploads/downloads using
* 🔌 protocols: [http](#the-browser) // [webdav](#webdav-server) // [ftp](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server)
* 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts)
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running from a basement in finland
👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running on a nuc in my basement
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) // 👉 **[feature-showcase](https://a.ocv.me/pub/demo/showcase-hq.webm)** ([youtube](https://www.youtube.com/watch?v=15_-hgsX2V0))
made in Norway 🇳🇴
## readme toc
@@ -54,6 +56,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
* [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/))
* [markdown viewer](#markdown-viewer) - and there are *two* editors
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
* [other tricks](#other-tricks)
@@ -77,6 +80,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [periodic rescan](#periodic-rescan) - filesystem monitoring
* [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
* [other flags](#other-flags)
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
@@ -104,7 +108,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
* [feature beefybits](#feature-beefybits) - force-enable features with known issues on your OS/env
* [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
* [fedora package](#fedora-package) - does not exist yet
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module)
@@ -255,7 +259,8 @@ also see [comparison to similar software](./docs/versus.md)
* ☑ play video files as audio (converted on server)
* ☑ create and play [m3u8 playlists](#playlists)
* ☑ image gallery with webm player
* ☑ textfile browser with syntax hilighting
*[textfile browser](#textfile-viewer) with syntax hilighting
* ☑ realtime streaming of growing files (logfiles and such)
* ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow, pyvips, or FFmpeg
* ☑ ...of videos using FFmpeg
@@ -417,6 +422,9 @@ upgrade notes
"frequently" asked questions
* CopyParty?
* nope! the name is either copyparty (all-lowercase) or Copyparty -- it's [one word](https://en.wiktionary.org/wiki/copyparty) after all :>
* can I change the 🌲 spinning pine-tree loading animation?
* [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-(
@@ -556,6 +564,8 @@ a client can request to see dotfiles in directory listings if global option `-ed
dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set
> even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab
config file example, where the same permission to see dotfiles is given in two different ways just for reference:
```yaml
@@ -692,7 +702,10 @@ enabling `multiselect` lets you click files to select them, and then shift-click
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
* the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel`
to show `/icons/exe.png` as the thumbnail for all .exe files, `--ext-th=exe=/icons/exe.png` (optionally as a volflag)
to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` and `.elf` files respectively, do this: `--ext-th=exe=/icons/exe.png --ext-th=elf=/icons/elf.gif`
* optionally as separate volflags for each mapping; see config file example below
* the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)
* be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however
config file example:
@@ -709,6 +722,7 @@ config file example:
dthumb # disable ALL thumbnails and audio transcoding
dvthumb # only disable video thumbnails
ext-th: exe=/ico/exe.png # /ico/exe.png is the thumbnail of *.exe
ext-th: elf=/ico/elf.gif # ...and /ico/elf.gif is used for *.elf
th-covers: folder.png,folder.jpg,cover.png,cover.jpg # the default
```
@@ -915,6 +929,7 @@ semi-intentional limitations:
* cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d`
* only folders from the same volume are shared; if you are sharing a folder which contains other volumes, then the contents of those volumes will not be available
* if you change [password hashing](#password-hashing) settings after creating a password-protected share, then that share will stop working
* related to [IdP volumes being forgotten on shutdown](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#idp-volumes-are-forgotten-on-shutdown), any shares pointing into a user's IdP volume will be unavailable until that user makes their first request after a restart
* no option to "delete after first access" because tricky
* when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
@@ -1115,6 +1130,18 @@ not available on iPhones / iPads because AudioContext currently breaks backgroun
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
## textfile viewer
with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/)) , and terminal colors work too
click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons:
* `✏️ edit` opens the textfile editor
* `📡 follow` starts monitoring the file for changes, streaming new lines in realtime
* similar to `tail -f`
* [link directly](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail) to a file with tailing enabled by adding `&tail` to the textviewer URL
## markdown viewer
and there are *two* editors
@@ -1413,12 +1440,17 @@ if you enable deduplication with `--dedup` then it'll create a symlink instead o
**warning:** when enabling dedup, you should also:
* enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended
* ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below
* ...and/or `--reflink` to use CoW/reflink-based dedup (much safer than hardlink, but OS/FS-dependent)
it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to)
by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`;
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`, and you can choose to use reflinks with `--reflink` or volflag `reflink`
advantages of using reflinks (CoW, copy-on-write):
* entirely safe (when your filesystem supports it correctly); either file can be edited or deleted without affecting other copies
* only linux 5.3 or newer, only python 3.14 or newer, only some filesystems (btrfs probably ok, maybe xfs too, but zfs had bugs)
advantages of using hardlinks:
* hardlinks are more compatible with other software; they behave entirely like regular files
@@ -1475,7 +1507,6 @@ the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`,
note:
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
config file example (these options are recommended btw):
@@ -1580,7 +1611,7 @@ config file example:
w: * # anyone can upload here
rw: ed # only user "ed" can read-write
flags:
e2ds: # filesystem indexing is required for many of these:
e2ds # filesystem indexing is required for many of these:
sz: 1k-3m # accept upload only if filesize in this range
df: 4g # free disk space cannot go lower than this
vmaxb: 1g # volume can never exceed 1 GiB
@@ -1619,6 +1650,26 @@ some examples,
allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4`
## chmod and chown
per-volume filesystem-permissions and ownership
by default:
* all folders are chmod 755
* files are usually chmod 644 (umask-defined)
* user/group is whatever copyparty is running as
this can be configured per-volume:
* volflag `chmod_f` sets file permissions; default=`644` (usually)
* volflag `chmod_d` sets directory permissions; default=`755`
* volflag `uid` sets the owner user-id
* volflag `gid` sets the owner group-id
notes:
* `gid` can only be set to one of the groups which the copyparty process is a member of
* `uid` can only be set if copyparty is running as root (i appreciate your faith)
## other flags
* `:c,magic` enables filetype detection for nameless uploads, same as `--magic`
@@ -1637,6 +1688,8 @@ this can instead be kept in a single place using the `--hist` argument, or the `
by default, the per-volume `up2k.db` sqlite3-database for `-e2d` and `-e2t` is stored next to the thumbnails according to the `--hist` option, but the global-option `--dbpath` and/or volflag `dbpath` can be used to put the database somewhere else
if your storage backend is unreliable (NFS or bad HDDs), you can specify one or more "landmarks" to look for before doing anything database-related. A landmark is a file which is always expected to exist inside the volume. This avoids spurious filesystem rescans in the event of an outage. One line per landmark (see example below)
note:
* putting the hist-folders on an SSD is strongly recommended for performance
* markdown edits are always stored in a local `.hist` subdirectory
@@ -1654,6 +1707,8 @@ config file example:
flags:
hist: - # restore the default (/mnt/nas/pics/.hist/)
hist: /mnt/nas/cache/pics/ # can be absolute path
landmark: me.jpg # /mnt/nas/pics/me.jpg must be readable to enable db
landmark: info/a.txt^=ok # and this textfile must start with "ok"
```
@@ -1993,7 +2048,7 @@ some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatical
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/tmp/party.sock` (permission `770` means only members of group `www` can access it)
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/dev/shm/party.sock` (permission `770` means only members of group `www` can access it)
example webserver / reverse-proxy configs:
@@ -2192,6 +2247,7 @@ force-enable features with known issues on your OS/env by setting any of the fo
| env-var | what it does |
| ------------------------ | ------------ |
| `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
| `PRTY_FORCE_MAGIC` | use [magic](https://pypi.org/project/python-magic/) on Windows (you will segfault) |
# packages
@@ -2203,14 +2259,18 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
## arch package
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
it comes with a [systemd service](./contrib/package/arch/copyparty.service) and expects to find one or more [config files](./docs/example.conf) in `/etc/copyparty.d/`
after installing it, you may want to `cp /usr/lib/systemd/system/copyparty.service /etc/systemd/system/` and then `vim /etc/systemd/system/copyparty.service` to change what user/group it is running as (you only need to do this once)
NOTE: there used to be an aur package; this evaporated when copyparty was adopted by the official archlinux repos. If you're still using the aur package, please move
## fedora package
does not exist yet; using the [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) builds is **NOT recommended** because updates can be delayed by [several months](https://github.com/fedora-copr/copr/issues/3056)
does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
## nix package
@@ -2335,8 +2395,10 @@ TLDR: yes
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| file search | - | yep | yep | yep | yep | yep | yep | yep |
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
| unpost uploads | - | - | yep | yep | yep | yep | yep | yep |
| navpane | - | yep | yep | yep | yep | yep | yep | yep |
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
| video player | - | yep | yep | yep | yep | yep | yep | yep |
@@ -2409,6 +2471,9 @@ interact with copyparty using non-browser clients
* and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu)
* and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh)
* [Custom Uploader](https://f-droid.org/en/packages/com.nyx.custom_uploader/) (an Android app) as an alternative to copyparty's own [PartyUP!](#android-app)
* works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f`
* contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson)
* [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f`
@@ -2510,6 +2575,11 @@ below are some tweaks roughly ordered by usefulness:
when uploading files,
* when uploading from very fast storage (NVMe SSD) with chrome/firefox, enable `[wasm]` in the `[⚙️] settings` tab to more effectively use all CPU-cores for hashing
* don't do this on Safari (runs faster without)
* don't do this on older browsers; likely to provoke browser-bugs (browser eats all RAM and crashes)
* can be made default-enabled serverside with `--nosubtle 137` (chrome v137+) or `--nosubtle 2` (chrome+firefox)
* chrome is recommended (unfortunately), at least compared to firefox:
* up to 90% faster when hashing, especially on SSDs
* up to 40% faster when uploading over extremely fast internets
@@ -2690,7 +2760,7 @@ enable [thumbnails](#thumbnails) of...
* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+)
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler)
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin`
* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` or pillow v11.3+
* **JPEG XL pictures:** `pyvips` or `ffmpeg`
enable sending [zeromq messages](#zeromq) from event-hooks: `pyzmq`
@@ -2717,10 +2787,11 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
| `PRTY_NO_MAGIC` | do not use [magic](https://pypi.org/project/python-magic/) for filetype detection |
| `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
| `PRTY_NO_PIL_AVIF` | disable 3rd-party Pillow plugin for [AVIF support](https://pypi.org/project/pillow-avif-plugin/) |
| `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) |
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pyheif-pillow-opener/) |
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
@@ -2817,5 +2888,7 @@ if there's a wall of base64 in the log (thread stacks) then please include that,
for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
specifically you may want to [build the sfx](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#just-the-sfx) or [build from scratch](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch)
see [./docs/TODO.md](./docs/TODO.md) for planned features / fixes / changes

View File

@@ -52,7 +52,7 @@ example usage as a volflag in a copyparty config file:
### CONFIG
# filetypes to process; ignores everything else
EXTS = "mp3 flac ogg opus m4a aac wav wma"
EXTS = "mp3 flac ogg oga opus m4a aac wav wma"
# the name of the subdir to put the normalized files in
SUBDIR = "normalized"

View File

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

View File

@@ -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,17 @@ 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
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)

View File

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

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "2.10"
S_BUILD_DT = "2025-02-19"
S_VERSION = "2.11"
S_BUILD_DT = "2025-05-18"
"""
u2c.py: upload to copyparty
@@ -1289,7 +1289,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,

View File

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

View File

@@ -1,21 +1,26 @@
{ 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))
else if isAttrs value then
@@ -49,13 +54,14 @@ let
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
'';
name = "copyparty";
cfg = config.services.copyparty;
configFile = pkgs.writeText "${name}.conf" configStr;
runtimeConfigPath = "/run/${name}/${name}.conf";
home = "/var/lib/${name}";
defaultShareDir = "${home}/data";
in {
configFile = pkgs.writeText "copyparty.conf" configStr;
runtimeConfigPath = "/run/copyparty/copyparty.conf";
externalCacheDir = "/var/cache/copyparty";
externalStateDir = "/var/lib/copyparty";
defaultShareDir = "${externalStateDir}/data";
in
{
options.services.copyparty = {
enable = mkEnableOption "web-based file manager";
@@ -68,6 +74,35 @@ in {
'';
};
mkHashWrapper = mkOption {
type = types.bool;
default = true;
description = ''
Make a shell script wrapper called 'copyparty-hash' with all options set here,
that launches the hashing cli.
'';
};
user = mkOption {
type = types.str;
default = "copyparty";
description = ''
The user that copyparty will run under.
If changed from default, you are responsible for making sure the user exists.
'';
};
group = mkOption {
type = types.str;
default = "copyparty";
description = ''
The group that copyparty will run under.
If changed from default, you are responsible for making sure the user exists.
'';
};
openFilesLimit = mkOption {
default = 4096;
type = types.either types.int types.str;
@@ -79,33 +114,41 @@ in {
description = ''
Global settings to apply.
Directly maps to values in the [global] section of the copyparty config.
Cannot set "c" or "hist", those are set by this module.
See `${getExe cfg.package} --help` for more details.
'';
default = {
i = "127.0.0.1";
no-reload = true;
hist = externalCacheDir;
};
example = literalExpression ''
{
i = "0.0.0.0";
no-reload = true;
hist = ${externalCacheDir};
}
'';
};
accounts = mkOption {
type = types.attrsOf (types.submodule ({ ... }: {
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.
'';
@@ -118,74 +161,81 @@ 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"];
type = types.attrsOf (
types.submodule (
{ ... }:
{
options = {
path = mkOption {
type = types.path;
description = ''
Path of a directory to share.
'';
};
'';
};
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$";
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"];
};
'';
};
'';
default = { };
};
};
}));
flags = mkOption {
type = types.attrs;
description = ''
Attribute list of volume flags to apply.
See `${getExe cfg.package} --help-flags` for more details.
'';
example = literalExpression ''
{
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
fk = 4;
# scan for new files every 60sec
scan = 60;
# volflag "e2d" enables the uploads database
e2d = true;
# "d2t" disables multimedia parsers (in case the uploads are malicious)
d2t = true;
# skips hashing file contents if path matches *.iso
nohash = "\.iso$";
};
'';
default = { };
};
};
}
)
);
description = "A set of copyparty volumes to create";
default = {
"/" = {
path = defaultShareDir;
access = { r = "*"; };
access = {
r = "*";
};
};
};
example = literalExpression ''
@@ -204,80 +254,122 @@ 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) cfg.volumes);
# ProtectSystem = "strict";
# Note that unlike what 'ro' implies,
# this actually makes it impossible to read anything in the root FS,
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
TemporaryFileSystem = "/:ro";
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
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 = ":755";
};
}
) cfg.volumes
);
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.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
description = "Service user for copyparty";
group = "copyparty";
home = externalStateDir;
isSystemUser = true;
};
};
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
(pkgs.writeShellScriptBin "copyparty-hash" ''
set -a # automatically export variables
# set same environment variables as the systemd service
${lib.pipe config.systemd.services.copyparty.environment [
(lib.filterAttrs (n: v: v != null && n != "PATH"))
(lib.mapAttrs (_: v: "${v}"))
(lib.toShellVars)
]}
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
users.groups.copyparty = { };
users.users.copyparty = {
description = "Service user for copyparty";
group = "copyparty";
home = home;
isSystemUser = true;
};
};
exec ${command} --ah-cli
'')
];
}
);
}

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty
pkgver="1.16.21"
pkgver="1.18.7"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -22,7 +22,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("2e416e18dc854c65643b8aaedca56e0a5c5a03b0c3d45b7ff3f68daa38d8e9c6")
sha256sums=("6738f623905276e8664bd8791f1497d0c61f8816c6608e76c4d9097a12849170")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,41 +1,67 @@
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, pyzmq, ffmpeg, mutagen,
{
lib,
stdenv,
makeWrapper,
fetchurl,
util-linux,
python,
jinja2,
impacket,
pyopenssl,
cfssl,
argon2-cffi,
pillow,
pyvips,
pyzmq,
ffmpeg,
mutagen,
# 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 FTPS support in the FTP server
withFTPS ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false,
# extra packages to add to the PATH
extraPackages ? [ ],
# function that accepts a python packageset and returns a list of packages to
# be added to the python venv. useful for scripts and such that require
# additional dependencies
extraPythonPackages ? (_p: [ ]),
}:
let
pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (ps:
with ps; [
pyEnv = python.withPackages (
ps:
with ps;
[
jinja2
]
++ lib.optional withSMB impacket
@@ -47,22 +73,36 @@ let
++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq
);
in stdenv.mkDerivation {
++ (extraPythonPackages ps)
);
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
stdenv.mkDerivation {
pname = "copyparty";
version = pinData.version;
inherit (pinData) version;
src = fetchurl {
url = pinData.url;
hash = pinData.hash;
inherit (pinData) url hash;
};
buildInputs = [ makeWrapper ];
nativeBuildInputs = [ 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"
--prefix PATH : ${lib.makeBinPath runtimeDeps} \
--add-flag $out/share/copyparty-sfx.py
'';
meta.mainProgram = "copyparty";
meta = {
description = "Turn almost any device into a file server";
longDescription = ''
Portable file server with accelerated resumable uploads, dedup, WebDAV,
FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
'';
homepage = "https://github.com/9001/copyparty";
changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}";
license = lib.licenses.mit;
inherit (python.meta) platforms;
mainProgram = "copyparty";
sourceProvenance = [ lib.sourceTypes.binaryBytecode ];
};
}

View File

@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.16.21/copyparty-sfx.py",
"version": "1.16.21",
"hash": "sha256-+/f4g8J2Mv0l6ChXzbNJ84G8LeB+mP1UfkWzQxizd/g="
"url": "https://github.com/9001/copyparty/releases/download/v1.18.7/copyparty-sfx.py",
"version": "1.18.7",
"hash": "sha256-GRoiNnYRWNcq5ITywPv7bK4pdgTey6Or1cZqyE2XDf4="
}

View File

@@ -0,0 +1,26 @@
{
stdenvNoCC,
copyparty,
python3,
makeBinaryWrapper,
}:
let
python = python3.withPackages (p: [ p.fusepy ]);
in
stdenvNoCC.mkDerivation {
pname = "partyfuse";
inherit (copyparty) version meta;
src = ../../../..;
nativeBuildInputs = [ makeBinaryWrapper ];
installPhase = ''
runHook preInstall
install -Dm444 bin/partyfuse.py -t $out/share/copyparty
makeWrapper ${python.interpreter} $out/bin/partyfuse \
--add-flag $out/share/copyparty/partyfuse.py
runHook postInstall
'';
}

View File

@@ -0,0 +1,24 @@
{
stdenvNoCC,
copyparty,
python312,
makeBinaryWrapper,
}:
stdenvNoCC.mkDerivation {
pname = "u2c";
inherit (copyparty) version meta;
src = ../../../..;
nativeBuildInputs = [ makeBinaryWrapper ];
installPhase = ''
runHook preInstall
install -Dm444 bin/u2c.py -t $out/share/copyparty
mkdir $out/bin
makeWrapper ${python312.interpreter} $out/bin/u2c \
--add-flag $out/share/copyparty/u2c.py
runHook postInstall
'';
}

View File

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

View File

@@ -80,6 +80,7 @@ web/deps/prismd.css
web/deps/scp.woff2
web/deps/sha512.ac.js
web/deps/sha512.hw.js
web/idp.html
web/iiam.gif
web/md.css
web/md.html

View File

@@ -53,13 +53,13 @@ from .util import (
PYFTPD_VER,
RAM_AVAIL,
RAM_TOTAL,
RE_ANSI,
SQLITE_VER,
UNPLICATIONS,
URL_BUG,
URL_PRJ,
Daemon,
align_tab,
ansi_re,
b64enc,
dedent,
has_resource,
@@ -167,7 +167,7 @@ def lprint(*a: Any, **ka: Any) -> None:
txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt)
if not VT100:
txt = ansi_re.sub("", txt)
txt = RE_ANSI.sub("", txt)
print(txt, end="", **ka)
@@ -547,14 +547,15 @@ def get_sects():
when running behind a reverse-proxy, it's recommended to
use unix-sockets for improved performance and security;
\033[32m-i unix:770:www:\033[33m/tmp/a.sock\033[0m listens on \033[33m/tmp/a.sock\033[0m with
permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m
group. This is the best approach. Alternatively,
\033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on
\033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m;
only accessible to members of the \033[33mwww\033[0m group.
This is the best approach. Alternatively,
\033[32m-i unix:777:\033[33m/tmp/a.sock\033[0m sets perms \033[33m0777\033[0m so anyone can
access it; bad unless it's inside a restricted folder
\033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone
can access it; bad unless it's inside a restricted folder
\033[32m-i unix:\033[33m/tmp/a.sock\033[0m keeps umask-defined permissions
\033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission
(usually \033[33m0600\033[0m) and the same user/group as copyparty
\033[33m-p\033[0m (tcp ports) is ignored for unix sockets
@@ -863,6 +864,43 @@ def get_sects():
"""
),
],
[
"chmod",
"file/folder permissions",
dedent(
"""
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
the value is a three-digit octal number such as \033[32m755\033[0m, \033[32m750\033[0m, \033[32m644\033[0m, etc.
first digit = "User"; permission for the unix-user
second digit = "Group"; permission for the unix-group
third digit = "Other"; permission for all other users/groups
for files:
\033[32m0\033[0m = \033[35m---\033[0m = no access
\033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
\033[32m2\033[0m = \033[35m-w-\033[0m = can write
\033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
\033[32m4\033[0m = \033[35mr--\033[0m = can read
\033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
\033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute
for directories/folders:
\033[32m0\033[0m = \033[35m---\033[0m = no access
\033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
\033[32m2\033[0m = \033[35m-w-\033[0m = n/a
\033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
\033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
\033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
\033[32m6\033[0m = \033[35mrw-\033[0m = n/a
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
"""
),
],
[
"pwhash",
"password hashing",
@@ -964,6 +1002,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
ap2.add_argument("--version", action="store_true", help="show versions and exit")
@@ -1003,16 +1042,24 @@ def add_upload(ap):
ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)")
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
@@ -1025,6 +1072,7 @@ def add_upload(ap):
ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck")
ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)")
ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed")
ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]")
ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)")
@@ -1044,7 +1092,7 @@ def add_network(ap):
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
else:
elif not MACOS:
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
@@ -1088,12 +1136,16 @@ def add_cert(ap, cert_path):
def add_auth(ap):
idp_db = os.path.join(E.cfg, "idp.db")
ses_db = os.path.join(E.cfg, "sessions.db")
ap2 = ap.add_argument_group('IdP / identity provider / user authentication options')
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)")
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
@@ -1240,6 +1292,7 @@ def add_stats(ap):
def add_yolo(ap):
ap2 = ap.add_argument_group('yolo options')
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
@@ -1266,6 +1319,7 @@ def add_optouts(ap):
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)")
ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
@@ -1286,6 +1340,7 @@ def add_safety(ap):
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
@@ -1379,8 +1434,8 @@ def add_thumbnail(ap):
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T,T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
@@ -1395,6 +1450,16 @@ def add_transcoding(ap):
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds")
def add_tail(ap):
ap2 = ap.add_argument_group('tailing options (realtime streaming of a growing file)')
ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)")
ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads")
ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)")
ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)")
ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect")
ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)")
def add_rss(ap):
ap2 = ap.add_argument_group('RSS options')
ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)")
@@ -1490,7 +1555,9 @@ def add_ui(ap, retry):
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
@@ -1504,7 +1571,7 @@ def add_ui(ap, retry):
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-np\033[0m")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
@@ -1514,6 +1581,7 @@ def add_ui(ap, retry):
ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes")
ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
ap2.add_argument("--have-unlistc", action="store_true", help=argparse.SUPPRESS)
def add_debug(ap):
@@ -1599,6 +1667,7 @@ def run_argparse(
add_hooks(ap)
add_stats(ap)
add_txt(ap)
add_tail(ap)
add_og(ap)
add_ui(ap, retry)
add_admin(ap)

View File

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

View File

@@ -21,6 +21,7 @@ from .util import (
DEF_MTE,
DEF_MTH,
EXTS,
HAVE_SQLITE3,
IMPLICATIONS,
MIMES,
SQLITE_VER,
@@ -32,6 +33,8 @@ from .util import (
afsenc,
get_df,
humansize,
json_hesc,
min_ex,
odfusion,
read_utf8,
relchk,
@@ -44,6 +47,9 @@ from .util import (
vsplit,
)
if HAVE_SQLITE3:
import sqlite3
if True: # pylint: disable=using-constant-test
from collections.abc import Iterable
@@ -65,6 +71,25 @@ if PY2:
LEELOO_DALLAS = "leeloo_dallas"
##
## you might be curious what Leeloo Dallas is doing here, so let me explain:
##
## certain daemonic tasks, namely:
## * deletion of expired files, running on a timer
## * deletion of sidecar files, initiated by plugins
## need to skip the usual permission-checks to do their thing,
## so we let Leeloo handle these
##
## and also, the smb-server has really shitty support for user-accounts
## so one popular way to avoid issues is by running copyparty without users;
## this makes all smb-clients identify as LD to gain unrestricted access
##
## Leeloo, being a fictional character from The Fifth Element,
## obviously does not exist and will never be able to access any copyparty
## instances from the outside (the username is rejected at every entrypoint)
##
## thanks for coming to my ted talk
SEE_LOG = "see log for details"
SEESLOG = " (see serverlog for details)"
@@ -72,7 +97,9 @@ SSEELOG = " ({})".format(SEE_LOG)
BAD_CFG = "invalid config; {}".format(SEE_LOG)
SBADCFG = " ({})".format(BAD_CFG)
PTN_U_GRP = re.compile(r"\$\{u%([+-])([^}]+)\}")
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
PTN_SIGIL = re.compile(r"(\${[ug][}%])")
class CfgEx(Exception):
@@ -113,6 +140,10 @@ class Lim(object):
self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry
self.chmod_d = 0o755
self.uid = self.gid = -1
self.chown = False
self.nups: dict[str, list[float]] = {} # num tracker
self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list
self.bupc: dict[str, int] = {} # byte tracker cache
@@ -273,7 +304,9 @@ class Lim(object):
if not dirs:
# no branches yet; make one
sub = os.path.join(path, "0")
bos.mkdir(sub)
bos.mkdir(sub, self.chmod_d)
if self.chown:
os.chown(sub, self.uid, self.gid)
else:
# try newest branch only
sub = os.path.join(path, str(dirs[-1]))
@@ -288,7 +321,9 @@ class Lim(object):
# make a branch
sub = os.path.join(path, str(dirs[-1] + 1))
bos.mkdir(sub)
bos.mkdir(sub, self.chmod_d)
if self.chown:
os.chown(sub, self.uid, self.gid)
ret = self.dive(sub, lvs - 1)
if ret is None:
raise Pebkac(500, "rotation bug")
@@ -357,7 +392,6 @@ class VFS(object):
self.flags = flags # config options
self.root = self
self.dev = 0 # st_dev
self.badcfg1 = False
self.nodes: dict[str, VFS] = {} # child nodes
self.histtab: dict[str, str] = {} # all realpath->histpath
self.dbpaths: dict[str, str] = {} # all realpath->dbpath
@@ -366,6 +400,7 @@ class VFS(object):
self.shr_src: Optional[tuple[VFS, str]] = None # source vfs+rem of a share
self.shr_files: set[str] = set() # filenames to include from shr_src
self.shr_owner: str = "" # uname
self.shr_all_aps: list[tuple[str, list[VFS]]] = []
self.aread: dict[str, list[str]] = {}
self.awrite: dict[str, list[str]] = {}
self.amove: dict[str, list[str]] = {}
@@ -377,20 +412,20 @@ class VFS(object):
self.adot: dict[str, list[str]] = {}
self.js_ls = {}
self.js_htm = ""
self.all_vols: dict[str, VFS] = {} # flattened recursive
self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares
if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
vp = vpath + ("/" if vpath else "")
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.dbpath = self.histpath
self.all_vols = {vpath: self} # flattened recursive
self.all_nodes = {vpath: self} # also jumpvols/shares
self.all_aps = [(rp, self)]
self.all_vols[vpath] = self
self.all_nodes[vpath] = self
self.all_aps = [(rp, [self])]
self.all_vps = [(vp, self)]
else:
self.histpath = self.dbpath = ""
self.all_vols = {}
self.all_nodes = {}
self.all_aps = []
self.all_vps = []
@@ -409,7 +444,7 @@ class VFS(object):
self,
vols: dict[str, "VFS"],
nodes: dict[str, "VFS"],
aps: list[tuple[str, "VFS"]],
aps: list[tuple[str, list["VFS"]]],
vps: list[tuple[str, "VFS"]],
) -> None:
nodes[self.vpath] = self
@@ -418,7 +453,11 @@ class VFS(object):
rp = self.realpath
rp += "" if rp.endswith(os.sep) else os.sep
vp = self.vpath + ("/" if self.vpath else "")
aps.append((rp, self))
hit = next((x[1] for x in aps if x[0] == rp), None)
if hit:
hit.append(self)
else:
aps.append((rp, [self]))
vps.append((vp, self))
for v in self.nodes.values():
@@ -842,9 +881,11 @@ class VFS(object):
return None
if "xvol" in self.flags:
for vap, vn in self.root.all_aps:
all_aps = self.shr_all_aps or self.root.all_aps
for vap, vns in all_aps:
if aps.startswith(vap):
return vn
return self if self in vns else vns[0]
if self.log:
self.log("vfs", "xvol: %r" % (ap,), 3)
@@ -853,6 +894,53 @@ class VFS(object):
return self
def check_landmarks(self) -> bool:
if self.dbv:
return True
vps = self.flags.get("landmark") or []
if not vps:
return True
failed = ""
for vp in vps:
if "^=" in vp:
vp, zs = vp.split("^=", 1)
expect = zs.encode("utf-8")
else:
expect = b""
if self.log:
t = "checking [/%s] landmark [%s]"
self.log("vfs", t % (self.vpath, vp), 6)
ap = "?"
try:
ap = self.canonical(vp)
with open(ap, "rb") as f:
buf = f.read(4096)
if not buf.startswith(expect):
t = "file [%s] does not start with the expected bytes %s"
failed = t % (ap, expect)
break
except Exception as ex:
t = "%r while trying to read [%s] => [%s]"
failed = t % (ex, vp, ap)
break
if not failed:
return True
if self.log:
t = "WARNING: landmark verification failed; %s; will now disable up2k database for volume [/%s]"
self.log("vfs", t % (failed, self.vpath), 3)
for rm in "e2d e2t e2v".split():
self.flags = {k: v for k, v in self.flags.items() if not k.startswith(rm)}
self.flags["d2d"] = True
self.flags["d2t"] = True
return False
if WINDOWS:
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
@@ -877,6 +965,7 @@ class AuthSrv(object):
self.warn_anonwrite = warn_anonwrite
self.line_ctr = 0
self.indent = ""
self.is_lxc = args.c == ["/z/initcfg"]
# fwd-decl
self.vfs = VFS(log_func, "", "", "", AXS(), {})
@@ -887,6 +976,8 @@ class AuthSrv(object):
self.defpw: dict[str, str] = {}
self.grps: dict[str, list[str]] = {}
self.re_pwd: Optional[re.Pattern] = None
self.cfg_files_loaded: list[str] = []
self.badcfg1 = False
# all volumes observed since last restart
self.idp_vols: dict[str, str] = {} # vpath->abspath
@@ -913,6 +1004,9 @@ class AuthSrv(object):
yield prev, True
def vf0(self):
return {"d2d": True, "tcolor": self.args.tcolor}
def idp_checkin(
self, broker: Optional["BrokerCli"], uname: str, gname: str
) -> bool:
@@ -931,6 +1025,10 @@ class AuthSrv(object):
return False
self.idp_accs[uname] = gnames
try:
self._update_idp_db(uname, gname)
except:
self.log("failed to update the --idp-db:\n%s" % (min_ex(),), 3)
t = "reinitializing due to new user from IdP: [%r:%r]"
self.log(t % (uname, gnames), 3)
@@ -943,6 +1041,22 @@ class AuthSrv(object):
broker.ask("reload", False, True).get()
return True
def _update_idp_db(self, uname: str, gname: str) -> None:
if not self.args.idp_store:
return
assert sqlite3 # type: ignore # !rm
db = sqlite3.connect(self.args.idp_db)
cur = db.cursor()
cur.execute("delete from us where un = ?", (uname,))
cur.execute("insert into us values (?,?)", (uname, gname))
db.commit()
cur.close()
db.close()
def _map_volume_idp(
self,
src: str,
@@ -963,15 +1077,27 @@ class AuthSrv(object):
un_gn = [("", "")]
for un, gn in un_gn:
m = PTN_U_GRP.search(dst0)
if m:
req, gnc = m.groups()
hit = gnc in (un_gns.get(un) or [])
if req == "+":
if not hit:
continue
elif hit:
rejected = False
for ptn in [PTN_U_GRP, PTN_G_GRP]:
m = ptn.search(dst0)
if not m:
continue
zs = m.group(1)
zs = zs.replace(",%+", "\n%+")
zs = zs.replace(",%-", "\n%-")
for rule in zs.split("\n"):
gnc = rule[2:]
if ptn == PTN_U_GRP:
# is user member of group?
hit = gnc in (un_gns.get(un) or [])
else:
# is it this specific group?
hit = gn == gnc
if rule.startswith("%+") != hit:
rejected = True
if rejected:
continue
# if ap/vp has a user/group placeholder, make sure to keep
# track so the same user/group is mapped when setting perms;
@@ -986,6 +1112,8 @@ class AuthSrv(object):
src = src1.replace("${g}", gn or "\n")
dst = dst1.replace("${g}", gn or "\n")
src = PTN_G_GRP.sub(gn or "\n", src)
dst = PTN_G_GRP.sub(gn or "\n", dst)
if src == src1 and dst == dst1:
gn = ""
@@ -1077,6 +1205,7 @@ class AuthSrv(object):
* any non-zero value from IdP group header
* otherwise take --grps / [groups]
"""
self.load_idp_db(bool(self.idp_accs))
ret = {un: gns[:] for un, gns in self.idp_accs.items()}
ret.update({zs: [""] for zs in acct if zs not in ret})
for gn, uns in grps.items():
@@ -1445,7 +1574,7 @@ class AuthSrv(object):
flags[name] = True
return
zs = "ext_th mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
zs = "ext_th landmark mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
if name not in zs.split():
if value is True:
t = "└─add volflag [{}] = {} ({})"
@@ -1482,8 +1611,10 @@ class AuthSrv(object):
daxs: dict[str, AXS] = {}
mflags: dict[str, dict[str, Any]] = {} # vpath:flags
mount: dict[str, tuple[str, str]] = {} # dst:src (vp:(ap,vp0))
cfg_files_loaded: list[str] = []
self.idp_vols = {} # yolo
self.badcfg1 = False
if self.args.a:
# list of username:password
@@ -1544,6 +1675,7 @@ class AuthSrv(object):
zst = [(max(0, len(x) - 2) * " ") + "" + x[-1] for x in zstt]
t = "loaded {} config files:\n{}"
self.log(t.format(len(zst), "\n".join(zst)))
cfg_files_loaded = zst
except:
lns = lns[: self.line_ctr]
@@ -1568,21 +1700,25 @@ class AuthSrv(object):
if not mount and not self.args.idp_h_usr:
# -h says our defaults are CWD at root and read/write for everyone
axs = AXS(["*"], ["*"], None, None)
if os.path.exists("/z/initcfg"):
t = "Read-access has been disabled due to failsafe: Docker detected, but the config does not define any volumes. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to all of /w/ by adding the following arguments to the docker container: -v .::rw"
self.log(t, 1)
if self.is_lxc:
t = "Read-access has been disabled due to failsafe: Docker detected, but %s. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to all of /w/ by adding the following arguments to the docker container: -v .::rw"
if len(cfg_files_loaded) == 1:
self.log(t % ("no config-file was provided",), 1)
t = "it is strongly recommended to add a config-file instead, for example based on https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/copyparty.conf"
self.log(t, 3)
else:
self.log(t % ("the config does not define any volumes",), 1)
axs = AXS()
elif self.args.c:
t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
self.log(t, 1)
axs = AXS()
vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0())
if not axs.uread:
vfs.badcfg1 = True
self.badcfg1 = True
elif "" not in mount:
# there's volumes but no root; make root inaccessible
zsd = {"d2d": True, "tcolor": self.args.tcolor}
vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
vfs = VFS(self.log_func, "", "", "", AXS(), self.vf0())
maxdepth = 0
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
@@ -1629,10 +1765,9 @@ class AuthSrv(object):
shr = enshare[1:-1]
shrs = enshare[1:]
if enshare:
import sqlite3
assert sqlite3 # type: ignore # !rm
zsd = {"d2d": True, "tcolor": self.args.tcolor}
shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
shv = VFS(self.log_func, "", shr, shr, AXS(), self.vf0())
db_path = self.args.shr_db
db = sqlite3.connect(db_path)
@@ -1852,7 +1987,7 @@ class AuthSrv(object):
is_shr = shr and zv.vpath.split("/")[0] == shr
if histp and not is_shr and histp in rhisttab:
zv2 = rhisttab[histp]
t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: %s [%s]"
t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]"
t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
self.log(t, 1)
raise Exception(t)
@@ -1866,7 +2001,7 @@ class AuthSrv(object):
is_shr = shr and zv.vpath.split("/")[0] == shr
if dbp and not is_shr and dbp in rdbpaths:
zv2 = rdbpaths[dbp]
t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2: %s [%s]"
t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]"
t = t % (dbp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath)
self.log(t, 1)
raise Exception(t)
@@ -2010,8 +2145,12 @@ class AuthSrv(object):
elif self.args.re_maxage:
vol.flags["scan"] = self.args.re_maxage
self.args.have_unlistc = False
all_mte = {}
errors = False
free_umask = False
have_reflink = False
for vol in vfs.all_nodes.values():
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True
@@ -2049,12 +2188,13 @@ class AuthSrv(object):
if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip nrand u2abort u2ow ups_who zip_who"
zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])
for k in ("convt",):
zs = "convt tail_fd tail_rate tail_tmax"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = float(vol.flags[k])
@@ -2067,13 +2207,53 @@ class AuthSrv(object):
t = 'volume "/%s" has invalid %stry [%s]'
raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
for k in ("chmod_d", "chmod_f"):
is_d = k == "chmod_d"
zs = vol.flags.get(k, "")
if not zs and is_d:
zs = "755"
if not zs:
vol.flags.pop(k, None)
continue
if not re.match("^[0-7]{3}$", zs):
t = "config-option '%s' must be a three-digit octal value such as [755] or [644] but the value was [%s]"
t = t % (k, zs)
self.log(t, 1)
raise Exception(t)
zi = int(zs, 8)
vol.flags[k] = zi
if (is_d and zi != 0o755) or not is_d:
free_umask = True
vol.flags.pop("chown", None)
if vol.flags["uid"] != -1 or vol.flags["gid"] != -1:
vol.flags["chown"] = True
vol.flags.pop("fperms", None)
if "chown" in vol.flags or vol.flags.get("chmod_f"):
vol.flags["fperms"] = True
if vol.lim:
vol.lim.chmod_d = vol.flags["chmod_d"]
vol.lim.chown = "chown" in vol.flags
vol.lim.uid = vol.flags["uid"]
vol.lim.gid = vol.flags["gid"]
if vol.flags.get("og"):
self.args.uqe = True
if "unlistcr" in vol.flags or "unlistcw" in vol.flags:
self.args.have_unlistc = True
if "reflink" in vol.flags:
have_reflink = True
zs = str(vol.flags.get("tcolor", "")).lstrip("#")
if len(zs) == 3: # fc5 => ffcc55
vol.flags["tcolor"] = "".join([x * 2 for x in zs])
# volflag syntax currently doesn't allow for ':' in value
zs = vol.flags["put_name"]
vol.flags["put_name2"] = zs.replace("{now.", "{now:.")
if vol.flags.get("neversymlink"):
vol.flags["hardlinkonly"] = True # was renamed
if vol.flags.get("hardlinkonly"):
@@ -2143,6 +2323,8 @@ class AuthSrv(object):
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
self.log(t % (vol.vpath, etv), 3)
vol.check_landmarks()
# d2d drops all database features for a volume
for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]:
if not vol.flags.get(grp, False):
@@ -2289,6 +2471,10 @@ class AuthSrv(object):
if errors:
sys.exit(1)
setattr(self.args, "free_umask", free_umask)
if free_umask:
os.umask(0)
vfs.bubble_flags()
have_e2d = False
@@ -2379,7 +2565,7 @@ class AuthSrv(object):
idp_vn, _ = vfs.get(idp_vp, "*", False, False)
idp_vp0 = idp_vn.vpath0
sigils = set(re.findall(r"(\${[ug][}%])", idp_vp0))
sigils = set(PTN_SIGIL.findall(idp_vp0))
if len(sigils) > 1:
t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
@@ -2424,11 +2610,19 @@ class AuthSrv(object):
t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
self.log(t + "".join(self.idp_err), 1)
if have_reflink:
t = "WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead."
if sys.version_info < (3, 14):
self.log(t % "your python version is not new enough", 1)
if not sys.platform.startswith("linux"):
self.log(t % "your OS is not Linux", 1)
self.vfs = vfs
self.acct = acct
self.defpw = defpw
self.grps = grps
self.iacct = {v: k for k, v in acct.items()}
self.cfg_files_loaded = cfg_files_loaded
self.load_sessions()
@@ -2490,6 +2684,28 @@ class AuthSrv(object):
shn.shr_src = (s_vfs, s_rem)
shn.realpath = s_vfs.canonical(s_rem)
# root.all_aps doesn't include any shares, so make a copy where the
# share appears in all abspaths it can provide (for example for chk_ap)
ap = shn.realpath
if not ap.endswith(os.sep):
ap += os.sep
shn.shr_all_aps = [(x, y[:]) for x, y in vfs.all_aps]
exact = False
for ap2, vns in shn.shr_all_aps:
if ap == ap2:
exact = True
if ap2.startswith(ap):
try:
vp2 = vjoin(s_rem, ap2[len(ap) :])
vn2, _ = s_vfs.get(vp2, "*", False, False)
if vn2 == s_vfs or vn2.dbv == s_vfs:
vns.append(shn)
except:
pass
if not exact:
shn.shr_all_aps.append((ap, [shn]))
shn.shr_all_aps.sort(key=lambda x: len(x[0]), reverse=True)
if self.args.shr_v:
t = "mapped %s share [%s] by [%s] => [%s] => [%s]"
self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath))
@@ -2504,7 +2720,7 @@ class AuthSrv(object):
continue # also fine
for zs in svn.nodes.keys():
# hide subvolume
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), self.vf0())
cur2.close()
cur.close()
@@ -2548,6 +2764,8 @@ class AuthSrv(object):
"txt_ext": self.args.textfiles.replace(",", " "),
"def_hcols": list(vf.get("mth") or []),
"unlist0": vf.get("unlist") or "",
"see_dots": self.args.see_dots,
"dqdel": self.args.qdel,
"dgrid": "grid" in vf,
"dgsel": "gsel" in vf,
"dnsort": "nsort" in vf,
@@ -2559,6 +2777,7 @@ class AuthSrv(object):
"idxh": int(self.args.ih),
"themes": self.args.themes,
"turbolvl": self.args.turbo,
"nosubtle": self.args.nosubtle,
"u2j": self.args.u2j,
"u2sz": self.args.u2sz,
"u2ts": vf["u2ts"],
@@ -2567,7 +2786,7 @@ class AuthSrv(object):
"lifetime": vn.js_ls["lifetime"],
"u2sort": self.args.u2sort,
}
vn.js_htm = json.dumps(js_htm)
vn.js_htm = json_hesc(json.dumps(js_htm))
vols = list(vfs.all_nodes.values())
if enshare:
@@ -2587,6 +2806,43 @@ class AuthSrv(object):
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
vol.flags["tcolor"] = zs.lstrip("#")
def load_idp_db(self, quiet=False) -> None:
# mutex me
level = self.args.idp_store
if level < 2 or not self.args.idp_h_usr:
return
assert sqlite3 # type: ignore # !rm
db = sqlite3.connect(self.args.idp_db)
cur = db.cursor()
from_cache = cur.execute("select un, gs from us").fetchall()
cur.close()
db.close()
self.idp_accs.clear()
self.idp_usr_gh.clear()
gsep = self.args.idp_gsep
n = []
for uname, gname in from_cache:
if level < 3:
if uname in self.idp_accs:
continue
gname = ""
gnames = [x.strip() for x in gsep.split(gname)]
gnames.sort()
# self.idp_usr_gh[uname] = gname
self.idp_accs[uname] = gnames
n.append(uname)
if n and not quiet:
t = ", ".join(n[:9])
if len(n) > 9:
t += "..."
self.log("found %d IdP users in db (%s)" % (len(n), t))
def load_sessions(self, quiet=False) -> None:
# mutex me
if self.args.no_ses:
@@ -2594,7 +2850,7 @@ class AuthSrv(object):
self.sesa = {}
return
import sqlite3
assert sqlite3 # type: ignore # !rm
ases = {}
blen = (self.args.ses_len // 4) * 4 # 3 bytes in 4 chars
@@ -2641,7 +2897,7 @@ class AuthSrv(object):
if self.args.no_ses:
return
import sqlite3
assert sqlite3 # type: ignore # !rm
db = sqlite3.connect(self.args.ses_db)
cur = db.cursor()

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ def vf_bmap() -> dict[str, str]:
"no_forget": "noforget",
"no_pipe": "nopipe",
"no_robots": "norobots",
"no_tail": "notail",
"no_thumb": "dthumb",
"no_vthumb": "dvthumb",
"no_athumb": "dathumb",
@@ -51,6 +52,8 @@ def vf_bmap() -> dict[str, str]:
"og_no_head",
"og_s_title",
"rand",
"reflink",
"rmagic",
"rss",
"wo_up_readme",
"xdev",
@@ -75,6 +78,9 @@ def vf_vmap() -> dict[str, str]:
"th_x3": "th3x",
}
for k in (
"bup_ck",
"chmod_d",
"chmod_f",
"dbd",
"forget_ip",
"hsortn",
@@ -95,13 +101,21 @@ def vf_vmap() -> dict[str, str]:
"og_title_i",
"og_tpl",
"og_ua",
"put_ck",
"put_name",
"mv_retry",
"rm_retry",
"sort",
"tail_fd",
"tail_rate",
"tail_tmax",
"tail_who",
"tcolor",
"unlist",
"u2abort",
"u2ts",
"uid",
"gid",
"ups_who",
"zip_who",
"zipmaxn",
@@ -157,14 +171,22 @@ flagcats = {
"dedup": "enable symlink-based file deduplication",
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
"safededup": "verify on-disk data before using it for dedup",
"noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)",
"chmod_d=755": "unix-permission for new dirs/folders",
"chmod_f=644": "unix-permission for new files",
"uid=573": "change owner of new files/folders to unix-user 573",
"gid=999": "change owner of new files/folders to unix-group 999",
"sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
"nosub": "forces all uploads into the top folder of the vfs",
"magic": "enables filetype detection for nameless uploads",
"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",
@@ -206,6 +228,7 @@ flagcats = {
"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",
@@ -268,6 +291,8 @@ flagcats = {
"nodirsz": "don't show total folder size",
"robots": "allows indexing by search engines (default)",
"norobots": "kindly asks search engines to leave",
"unlistcr": "don't list read-access in controlpanel",
"unlistcw": "don't list write-access in controlpanel",
"no_sb_md": "disable js sandbox for markdown files",
"no_sb_lg": "disable js sandbox for prologue/epilogue",
"sb_md": "enable js sandbox for markdown files (default)",
@@ -298,6 +323,13 @@ flagcats = {
"exp_md": "placeholders to expand in markdown files; see --help",
"exp_lg": "placeholders to expand in prologue/epilogue; see --help",
},
"tailing": {
"notail": "disable ?tail (download a growing file continuously)",
"tail_fd=1": "check if file was replaced (new fd) every 1 sec",
"tail_rate=0.2": "check for new data every 0.2 sec",
"tail_tmax=30": "kill connection after 30 sec",
"tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)",
},
"others": {
"dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
"fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
@@ -306,6 +338,7 @@ flagcats = {
"dks": "per-directory accesskeys allow browsing into subdirs",
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
"rss": "allow '?rss' URL suffix (experimental)",
"rmagic": "expensive analysis for mimetype accuracy",
"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",

View File

@@ -31,6 +31,7 @@ from .util import (
relchk,
runhook,
sanitize_fn,
set_fperms,
vjoin,
wunlink,
)
@@ -229,7 +230,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:
@@ -261,7 +262,11 @@ class FtpFs(AbstractedFS):
wunlink(self.log, ap, VF_CAREFUL)
return open(fsenc(ap), mode, self.args.iobuf)
ret = open(fsenc(ap), mode, self.args.iobuf)
if w and "fperms" in vfs.flags:
set_fperms(ret, vfs.flags)
return ret
def chdir(self, path: str) -> None:
nwd = join(self.cwd, path)
@@ -292,8 +297,8 @@ class FtpFs(AbstractedFS):
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:
ap = self.rv2a(path, w=True)[0]
bos.makedirs(ap) # filezilla expects this
ap, vfs, _ = self.rv2a(path, w=True)
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path)

View File

@@ -33,7 +33,7 @@ except:
from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION
from .authsrv import VFS # typechk
from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos
from .star import StreamTar
from .stolen.qrcodegen import QrCode, qr2svg
@@ -45,6 +45,7 @@ from .util import (
APPLESAN_RE,
BITNESS,
DAV_ALLPROPS,
E_SCK_WR,
FN_EMB,
HAVE_SQLITE3,
HTTPCODE,
@@ -78,8 +79,10 @@ from .util import (
hidedir,
html_bescape,
html_escape,
html_sh_esc,
humansize,
ipnorm,
json_hesc,
justcopy,
load_resource,
loadpy,
@@ -102,6 +105,7 @@ from .util import (
sanitize_vpath,
sendfile_kern,
sendfile_py,
set_fperms,
stat_resource,
ub64dec,
ub64enc,
@@ -113,7 +117,6 @@ from .util import (
vol_san,
vroots,
vsplit,
wrename,
wunlink,
yieldfile,
)
@@ -190,11 +193,11 @@ class HttpCli(object):
self.log_src = conn.log_src # mypy404
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
self.tls: bool = hasattr(self.s, "cipher")
self.is_vproxied = bool(self.args.R)
# placeholders; assigned by run()
self.keepalive = False
self.is_https = False
self.is_vproxied = False
self.in_hdr_recv = True
self.headers: dict[str, str] = {}
self.mode = " " # http verb
@@ -402,7 +405,6 @@ class HttpCli(object):
self.bad_xff = True
else:
self.ip = cli_ip
self.is_vproxied = bool(self.args.R)
self.log_src = self.conn.set_rproxy(self.ip)
self.host = self.headers.get("x-forwarded-host") or self.host
trusted_xff = True
@@ -535,6 +537,7 @@ class HttpCli(object):
else:
t = "incorrect --rp-loc or webserver config; expected vpath starting with %r but got %r"
self.log(t % (self.args.R, vpath), 1)
self.is_vproxied = False
self.ouparam = uparam.copy()
@@ -622,6 +625,9 @@ class HttpCli(object):
) or self.args.idp_h_key in self.headers
if trusted_key and trusted_xff:
if idp_usr.lower() == LEELOO_DALLAS:
self.loud_reply("send her back", status=403)
return False
self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp)
else:
if not trusted_key:
@@ -1110,15 +1116,18 @@ class HttpCli(object):
else:
return True
host = self.host.lower()
if host.startswith("["):
if "]:" in host:
host = host.split("]:")[0] + "]"
else:
host = host.split(":")[0]
oh = self.out_headers
origin = origin.lower()
good_origins = self.args.acao + [
"%s://%s"
% (
"https" if self.is_https else "http",
self.host.lower().split(":")[0],
)
]
proto = "https" if self.is_https else "http"
good_origins = self.args.acao + ["%s://%s" % (proto, host)]
if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins:
good_origin = True
bad_hdrs = ("",)
@@ -1234,10 +1243,19 @@ class HttpCli(object):
else:
return self.tx_404(True)
else:
vfs = self.asrv.vfs
if vfs.badcfg1:
t = "<h2>access denied due to failsafe; check server log</h2>"
html = self.j2s("splash", this=self, msg=t)
if (
self.asrv.badcfg1
and "h" not in self.ouparam
and "hc" not in self.ouparam
):
zs1 = "copyparty refused to start due to a failsafe: invalid server config; check server log"
zs2 = 'you may <a href="/?h">access the controlpanel</a> but nothing will work until you shutdown the copyparty container and %s config-file (or provide the configuration as command-line arguments)'
if self.asrv.is_lxc and len(self.asrv.cfg_files_loaded) == 1:
zs2 = zs2 % ("add a",)
else:
zs2 = zs2 % ("fix the",)
html = self.j2s("msg", h1=zs1, h2=zs2)
self.reply(html.encode("utf-8", "replace"), 500)
return True
@@ -1291,6 +1309,9 @@ class HttpCli(object):
if "ru" in self.uparam:
return self.tx_rups()
if "idp" in self.uparam:
return self.tx_idp()
if "h" in self.uparam:
return self.tx_mounts()
@@ -1363,12 +1384,13 @@ class HttpCli(object):
title = self.uparam.get("title") or self.vpath.split("/")[-1]
etitle = html_escape(title, True, True)
baseurl = "%s://%s%s" % (
baseurl = "%s://%s/" % (
"https" if self.is_https else "http",
self.host,
self.args.SRS,
)
feed = "%s%s" % (baseurl, self.req[1:])
feed = baseurl + self.req[1:]
if self.is_vproxied:
baseurl += self.args.RS
efeed = html_escape(feed, True, True)
edirlink = efeed.split("?")[0] + q_pw
@@ -1381,7 +1403,7 @@ class HttpCli(object):
\t\t<title>%s</title>
\t\t<description></description>
\t\t<link>%s</link>
\t\t<generator>copyparty-1</generator>
\t\t<generator>copyparty-2</generator>
"""
% (efeed, etitle, edirlink)
]
@@ -1404,7 +1426,13 @@ class HttpCli(object):
except:
pass
ap = ""
use_magic = "rmagic" in self.vn.flags
for i in hits:
if use_magic:
ap = os.path.join(self.vn.realpath, i["rp"])
iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True)
title = unquotep(i["rp"].split("?")[0].split("/")[-1])
title = html_escape(title, True, True)
@@ -1412,8 +1440,8 @@ class HttpCli(object):
tag_a = str(i["tags"].get("artist") or "")
desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a)
desc = html_escape(desc, True, True) if desc else title
mime = html_escape(guess_mime(title))
lmod = formatdate(i["ts"])
mime = html_escape(guess_mime(title, ap))
lmod = formatdate(max(0, i["ts"]))
zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
zs = (
"""\
@@ -1556,6 +1584,18 @@ class HttpCli(object):
self.log("inaccessible: %r" % ("/" + self.vpath,))
raise Pebkac(401, "authenticate")
if "quota-available-bytes" in props and not self.args.nid:
bfree, btot, _ = get_df(vn.realpath, False)
if btot:
df = {
"quota-available-bytes": str(bfree),
"quota-used-bytes": str(btot - bfree),
}
else:
df = {}
else:
df = {}
fgen = itertools.chain([topdir], fgen)
vtop = vjoin(self.args.R, vjoin(vn.vpath, rem))
@@ -1565,12 +1605,15 @@ class HttpCli(object):
None, 207, "text/xml; charset=" + enc, {"Transfer-Encoding": "chunked"}
)
ap = ""
use_magic = "rmagic" in vn.flags
ret = '<?xml version="1.0" encoding="{}"?>\n<D:multistatus xmlns:D="DAV:">'
ret = ret.format(uenc)
for x in fgen:
rp = vjoin(vtop, x["vp"])
st: os.stat_result = x["st"]
mtime = st.st_mtime
mtime = max(0, st.st_mtime)
if stat.S_ISLNK(st.st_mode):
try:
st = bos.stat(os.path.join(tap, x["vp"]))
@@ -1591,8 +1634,13 @@ class HttpCli(object):
"supportedlock": '<D:lockentry xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>',
}
if not isdir:
pvs["getcontenttype"] = html_escape(guess_mime(rp))
if use_magic:
ap = os.path.join(tap, x["vp"])
pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
pvs["getcontentlength"] = str(st.st_size)
elif df:
pvs.update(df)
df = {}
for k, v in pvs.items():
if k not in props:
@@ -2044,7 +2092,7 @@ class HttpCli(object):
fdir, fn = os.path.split(fdir)
rem, _ = vsplit(rem)
bos.makedirs(fdir)
bos.makedirs(fdir, vf=vfs.flags)
open_ka: dict[str, Any] = {"fun": open}
open_a = ["wb", self.args.iobuf]
@@ -2100,10 +2148,9 @@ class HttpCli(object):
suffix = "-{:.6f}-{}".format(time.time(), self.dip())
nameless = not fn
if nameless:
suffix += ".bin"
fn = "put" + suffix
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
params = {"suffix": suffix, "fdir": fdir}
params = {"suffix": suffix, "fdir": fdir, "vf": vfs.flags}
if self.args.nw:
params = {}
fn = os.devnull
@@ -2152,7 +2199,7 @@ class HttpCli(object):
if self.args.nw:
fn = os.devnull
else:
bos.makedirs(fdir)
bos.makedirs(fdir, vf=vfs.flags)
path = os.path.join(fdir, fn)
if not nameless:
self.vpath = vjoin(self.vpath, fn)
@@ -2181,28 +2228,26 @@ class HttpCli(object):
# small toctou, but better than clobbering a hardlink
wunlink(self.log, path, vfs.flags)
halg = "sha512"
hasher = None
copier = hashcopy
if "ck" in self.ouparam or "ck" in self.headers:
halg = zs = self.ouparam.get("ck") or self.headers.get("ck") or ""
if not zs or zs == "no":
copier = justcopy
halg = ""
elif zs == "md5":
hasher = hashlib.md5(**USED4SEC)
elif zs == "sha1":
hasher = hashlib.sha1(**USED4SEC)
elif zs == "sha256":
hasher = hashlib.sha256(**USED4SEC)
elif zs in ("blake2", "b2"):
hasher = hashlib.blake2b(**USED4SEC)
elif zs in ("blake2s", "b2s"):
hasher = hashlib.blake2s(**USED4SEC)
elif zs == "sha512":
pass
else:
raise Pebkac(500, "unknown hash alg")
halg = self.ouparam.get("ck") or self.headers.get("ck") or vfs.flags["put_ck"]
if halg == "sha512":
pass
elif halg == "no":
copier = justcopy
halg = ""
elif halg == "md5":
hasher = hashlib.md5(**USED4SEC)
elif halg == "sha1":
hasher = hashlib.sha1(**USED4SEC)
elif halg == "sha256":
hasher = hashlib.sha256(**USED4SEC)
elif halg in ("blake2", "b2"):
hasher = hashlib.blake2b(**USED4SEC)
elif halg in ("blake2s", "b2s"):
hasher = hashlib.blake2s(**USED4SEC)
else:
raise Pebkac(500, "unknown hash alg")
f, fn = ren_open(fn, *open_a, **params)
try:
@@ -2286,7 +2331,7 @@ class HttpCli(object):
if self.args.hook_v:
log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
fdir, self.vpath, fn, (vfs, rem) = x
bos.makedirs(fdir)
bos.makedirs(fdir, vf=vfs.flags)
path2 = os.path.join(fdir, fn)
atomic_move(self.log, path, path2, vfs.flags)
path = path2
@@ -2572,7 +2617,7 @@ class HttpCli(object):
dst = vfs.canonical(rem)
try:
if not bos.path.isdir(dst):
bos.makedirs(dst)
bos.makedirs(dst, vf=vfs.flags)
except OSError as ex:
self.log("makedirs failed %r" % (dst,))
if not bos.path.isdir(dst):
@@ -2591,10 +2636,6 @@ class HttpCli(object):
x = self.conn.hsrv.broker.ask("up2k.handle_json", body, self.u2fh.aps)
ret = x.get()
if self.is_vproxied:
if "purl" in ret:
ret["purl"] = self.args.SR + ret["purl"]
if self.args.shr and self.vpath.startswith(self.args.shr1):
# strip common suffix (uploader's folder structure)
vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
@@ -2604,6 +2645,10 @@ class HttpCli(object):
raise Pebkac(500, t % zt)
ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
if self.is_vproxied:
if "purl" in ret:
ret["purl"] = self.args.SR + ret["purl"]
ret = json.dumps(ret)
self.log(ret)
self.reply(ret.encode("utf-8"), mime="application/json")
@@ -2711,6 +2756,7 @@ class HttpCli(object):
locked = chashes # remaining chunks to be received in this request
written = [] # chunks written to disk, but not yet released by up2k
num_left = -1 # num chunks left according to most recent up2k release
bail1 = False # used in sad path to avoid contradicting error-text
treport = time.time() # ratelimit up2k reporting to reduce overhead
if "x-up2k-subc" in self.headers:
@@ -2849,7 +2895,6 @@ class HttpCli(object):
except:
# maybe busted handle (eg. disk went full)
f.close()
chashes = [] # exception flag
raise
finally:
if locked:
@@ -2858,13 +2903,14 @@ class HttpCli(object):
num_left, t = x.get()
if num_left < 0:
self.loud_reply(t, status=500)
if chashes: # kills exception bubbling otherwise
return False
bail1 = True
else:
t = "got %d more chunks, %d left"
self.log(t % (len(written), num_left), 6)
if num_left < 0:
if bail1:
return False
raise Pebkac(500, "unconfirmed; see serverlog")
if not num_left and fpool:
@@ -2888,6 +2934,7 @@ class HttpCli(object):
ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
if ok:
self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
ok, msg = self.get_pwd_cookie(pwd)
if ok:
msg = "new password OK"
@@ -2931,7 +2978,8 @@ class HttpCli(object):
self.parser.drop()
self.log("logout " + self.uname)
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
if not self.uname.startswith("s_"):
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
self.get_pwd_cookie("x")
dst = self.args.SRS + "?h"
@@ -2970,12 +3018,20 @@ class HttpCli(object):
# reset both plaintext and tls
# (only affects active tls cookies when tls)
for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
ck = gencookie(k, pwd, self.args.R, False)
ck = gencookie(k, pwd, self.args.R, self.args.cookie_lax, False)
self.out_headerlist.append(("Set-Cookie", ck))
self.out_headers.pop("Set-Cookie", None) # drop keepalive
else:
k = "cppws" if self.is_https else "cppwd"
ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly")
ck = gencookie(
k,
pwd,
self.args.R,
self.args.cookie_lax,
self.is_https,
dur,
"; HttpOnly",
)
self.out_headers["Set-Cookie"] = ck
return dur > 0, msg
@@ -2992,6 +3048,9 @@ class HttpCli(object):
self.gctx = vpath
vpath = undot(vpath)
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
if "nosub" in vfs.flags:
raise Pebkac(403, "mkdir is forbidden below this folder")
rem = sanitize_vpath(rem, "/")
fn = vfs.canonical(rem)
@@ -3005,7 +3064,7 @@ class HttpCli(object):
raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
try:
bos.makedirs(fn)
bos.makedirs(fn, vf=vfs.flags)
except OSError as ex:
if ex.errno == errno.EACCES:
raise Pebkac(500, "the server OS denied write-access")
@@ -3047,6 +3106,8 @@ class HttpCli(object):
with open(fsenc(fn), "wb") as f:
f.write(b"`GRUNNUR`\n")
if "fperms" in vfs.flags:
set_fperms(f, vfs.flags)
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.redirect(vpath, "?edit")
@@ -3084,15 +3145,18 @@ class HttpCli(object):
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
self._assert_safe_rem(rem)
halg = "sha512"
hasher = None
copier = hashcopy
if nohash:
halg = ""
copier = justcopy
elif "ck" in self.ouparam or "ck" in self.headers:
halg = self.ouparam.get("ck") or self.headers.get("ck") or ""
if not halg or halg == "no":
else:
copier = hashcopy
halg = (
self.ouparam.get("ck") or self.headers.get("ck") or vfs.flags["bup_ck"]
)
if halg == "sha512":
pass
elif halg == "no":
copier = justcopy
halg = ""
elif halg == "md5":
@@ -3105,8 +3169,6 @@ class HttpCli(object):
hasher = hashlib.blake2b(**USED4SEC)
elif halg in ("blake2s", "b2s"):
hasher = hashlib.blake2s(**USED4SEC)
elif halg == "sha512":
pass
else:
raise Pebkac(500, "unknown hash alg")
@@ -3119,7 +3181,7 @@ class HttpCli(object):
)
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
if not nullwrite:
bos.makedirs(fdir_base)
bos.makedirs(fdir_base, vf=vfs.flags)
rnd, lifetime, xbu, xau = self.upload_flags(vfs)
zs = self.uparam.get("want") or self.headers.get("accept") or ""
@@ -3152,7 +3214,7 @@ class HttpCli(object):
if rnd:
fname = rand_name(fdir, fname, rnd)
open_args = {"fdir": fdir, "suffix": suffix}
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
if "replace" in self.uparam:
if not self.can_delete:
@@ -3215,7 +3277,7 @@ class HttpCli(object):
open_args["fdir"] = fdir
if p_file and not nullwrite:
bos.makedirs(fdir)
bos.makedirs(fdir, vf=vfs.flags)
# reserve destination filename
f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
@@ -3319,7 +3381,7 @@ class HttpCli(object):
if nullwrite:
fdir = ap2 = ""
else:
bos.makedirs(fdir)
bos.makedirs(fdir, vf=vfs.flags)
atomic_move(self.log, abspath, ap2, vfs.flags)
abspath = ap2
sz = bos.path.getsize(abspath)
@@ -3440,6 +3502,8 @@ class HttpCli(object):
ft = "{}:{}".format(self.ip, self.addr[1])
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
f.write(ft.encode("utf-8"))
if "fperms" in vfs.flags:
set_fperms(f, vfs.flags)
except Exception as ex:
suf = "\nfailed to write the upload report: {}".format(ex)
@@ -3490,7 +3554,7 @@ class HttpCli(object):
lim = vfs.get_dbv(rem)[0].lim
if lim:
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
bos.makedirs(fp)
bos.makedirs(fp, vf=vfs.flags)
fp = os.path.join(fp, fn)
rem = "{}/{}".format(rp, fn).strip("/")
@@ -3558,18 +3622,22 @@ class HttpCli(object):
zs = ub64enc(zb).decode("ascii")[:24].lower()
dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs)
self.log("moving old version to %s/%s" % (dp, mfile2))
if bos.makedirs(dp):
if bos.makedirs(dp, vf=vfs.flags):
with open(os.path.join(dp, "dir.txt"), "wb") as f:
f.write(afsenc(vrd))
if "fperms" in vfs.flags:
set_fperms(f, vfs.flags)
elif hist_cfg == "s":
dp = os.path.join(mdir, ".hist")
try:
bos.mkdir(dp)
bos.mkdir(dp, vfs.flags["chmod_d"])
if "chown" in vfs.flags:
bos.chown(dp, vfs.flags["uid"], vfs.flags["gid"])
hidedir(dp)
except:
pass
if dp:
wrename(self.log, fp, os.path.join(dp, mfile2), vfs.flags)
atomic_move(self.log, fp, os.path.join(dp, mfile2), vfs.flags)
assert self.parser.gen # !rm
p_field, _, p_data = next(self.parser.gen)
@@ -3603,6 +3671,8 @@ class HttpCli(object):
wunlink(self.log, fp, vfs.flags)
with open(fsenc(fp), "wb", self.args.iobuf) as f:
if "fperms" in vfs.flags:
set_fperms(f, vfs.flags)
sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)
if lim:
@@ -3806,6 +3876,20 @@ class HttpCli(object):
return txt
def _can_tail(self, volflags: dict[str, Any]) -> bool:
zp = self.args.ua_nodoc
if zp and zp.search(self.ua):
t = "this URL contains no valuable information for bots/crawlers"
raise Pebkac(403, t)
lvl = volflags["tail_who"]
if "notail" in volflags or not lvl:
raise Pebkac(400, "tail is disabled in server config")
elif lvl <= 1 and not self.can_admin:
raise Pebkac(400, "tail is admin-only on this server")
elif lvl <= 2 and self.uname in ("", "*"):
raise Pebkac(400, "you must be authenticated to use ?tail on this server")
return True
def _can_zip(self, volflags: dict[str, Any]) -> str:
lvl = volflags["zip_who"]
if self.args.no_zip or not lvl:
@@ -3950,6 +4034,8 @@ class HttpCli(object):
logmsg = "{:4} {} ".format("", self.req)
logtail = ""
is_tail = "tail" in self.uparam and self._can_tail(self.vn.flags)
if ptop is not None:
ap_data = "<%s>" % (req_path,)
try:
@@ -3980,7 +4066,7 @@ class HttpCli(object):
if ptop is not None:
assert job and ap_data # type: ignore # !rm
sz = job["size"]
file_ts = job["lmod"]
file_ts = max(0, job["lmod"])
editions["plain"] = (ap_data, sz)
break
@@ -4063,6 +4149,7 @@ class HttpCli(object):
and can_range
and file_sz
and "," not in hrange
and not is_tail
):
try:
if not hrange.lower().startswith("bytes"):
@@ -4131,6 +4218,8 @@ class HttpCli(object):
mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
elif "mime" in self.uparam:
mime = str(self.uparam.get("mime"))
elif "rmagic" in self.vn.flags:
mime = guess_mime(req_path, fs_path)
else:
mime = guess_mime(req_path)
@@ -4148,13 +4237,18 @@ class HttpCli(object):
return True
dls = self.conn.hsrv.dls
if is_tail:
upper = 1 << 30
if len(dls) > self.args.tail_cmax:
raise Pebkac(400, "too many active downloads to start a new tail")
if upper - lower > 0x400000: # 4m
now = time.time()
self.dl_id = "%s:%s" % (self.ip, self.addr[1])
dls[self.dl_id] = (now, 0)
self.conn.hsrv.dli[self.dl_id] = (
now,
upper - lower,
0 if is_tail else upper - lower,
self.vn,
self.vpath,
self.uname,
@@ -4165,6 +4259,9 @@ class HttpCli(object):
return self.tx_pipe(
ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
)
elif is_tail:
self.tx_tail(open_args, status, mime)
return False
ret = True
with open_func(*open_args) as f:
@@ -4194,6 +4291,133 @@ class HttpCli(object):
return ret
def tx_tail(
self,
open_args: list[Any],
status: int,
mime: str,
) -> None:
vf = self.vn.flags
self.send_headers(length=None, status=status, mime=mime)
abspath: bytes = open_args[0]
sec_rate = vf["tail_rate"]
sec_max = vf["tail_tmax"]
sec_fd = vf["tail_fd"]
sec_ka = self.args.tail_ka
wr_slp = self.args.s_wr_slp
wr_sz = self.args.s_wr_sz
dls = self.conn.hsrv.dls
dl_id = self.dl_id
# non-numeric = full file from start
# positive = absolute offset from start
# negative = start that many bytes from eof
try:
ofs = int(self.uparam["tail"])
except:
ofs = 0
t0 = time.time()
ofs0 = ofs
f = None
try:
st = os.stat(abspath)
f = open(*open_args)
f.seek(0, os.SEEK_END)
eof = f.tell()
f.seek(0)
if ofs < 0:
ofs = max(0, ofs + eof)
self.log("tailing from byte %d: %r" % (ofs, abspath), 6)
# send initial data asap
remains = sendfile_py(
self.log, # d/c
ofs,
eof,
f,
self.s,
wr_sz,
wr_slp,
False, # d/c
dls,
dl_id,
)
sent = (eof - ofs) - remains
ofs = eof - remains
f.seek(ofs)
try:
st2 = os.stat(open_args[0])
if st.st_ino == st2.st_ino:
st = st2 # for filesize
except:
pass
gone = 0
t_fd = t_ka = time.time()
while True:
assert f # !rm
buf = f.read(4096)
now = time.time()
if sec_max and now - t0 >= sec_max:
self.log("max duration exceeded; kicking client", 6)
zb = b"\n\n*** max duration exceeded; disconnecting ***\n"
self.s.sendall(zb)
break
if buf:
t_fd = t_ka = now
self.s.sendall(buf)
sent += len(buf)
dls[dl_id] = (time.time(), sent)
continue
time.sleep(sec_rate)
if t_ka < now - sec_ka:
t_ka = now
self.s.send(b"\x00")
if t_fd < now - sec_fd:
try:
st2 = os.stat(open_args[0])
if (
st2.st_ino != st.st_ino
or st2.st_size < sent
or st2.st_size < st.st_size
):
assert f # !rm
# open new file before closing previous to avoid toctous (open may fail; cannot null f before)
f2 = open(*open_args)
f.close()
f = f2
f.seek(0, os.SEEK_END)
eof = f.tell()
if eof < sent:
ofs = sent = 0 # shrunk; send from start
zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
self.s.sendall(zb)
if ofs0 < 0 and eof > -ofs0:
ofs = eof + ofs0
else:
ofs = sent # just new fd? resume from same ofs
f.seek(ofs)
self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
gone = 0
st = st2
except:
gone += 1
if gone > 3:
self.log("file deleted; disconnecting")
break
except IOError as ex:
if ex.errno not in E_SCK_WR:
raise
finally:
if f:
f.close()
def tx_pipe(
self,
ptop: str,
@@ -4674,18 +4898,23 @@ class HttpCli(object):
def tx_svcs(self) -> bool:
aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a"
ep = self.host
host = ep.split(":")[0]
hport = ep[ep.find(":") :] if ":" in ep else ""
rip = (
host
if self.args.rclone_mdns or not self.args.zm
else self.conn.hsrv.nm.map(self.ip) or host
)
# safer than html_escape/quotep since this avoids both XSS and shell-stuff
pw = re.sub(r"[<>&$?`\"']", "_", self.pw or "hunter2")
vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/")
pw = pw.replace(" ", "%20")
vp = vp.replace(" ", "%20")
sep = "]:" if "]" in ep else ":"
if sep in ep:
host, hport = ep.rsplit(":", 1)
hport = ":" + hport
else:
host = ep
hport = ""
if host.endswith(".local") and self.args.zm and not self.args.rclone_mdns:
rip = self.conn.hsrv.nm.map(self.ip) or host
if ":" in rip and "[" not in rip:
rip = "[%s]" % (rip,)
else:
rip = host
vp = (self.uparam["hc"] or "").lstrip("/")
pw = self.pw or "hunter2"
if pw in self.asrv.sesa:
pw = "hunter2"
@@ -4694,14 +4923,14 @@ class HttpCli(object):
args=self.args,
accs=bool(self.asrv.acct),
s="s" if self.is_https else "",
rip=rip,
ep=ep,
vp=vp,
rvp=vjoin(self.args.R, vp),
host=host,
hport=hport,
rip=html_sh_esc(rip),
ep=html_sh_esc(ep),
vp=html_sh_esc(vp),
rvp=html_sh_esc(vjoin(self.args.R, vp)),
host=html_sh_esc(host),
hport=html_sh_esc(hport),
aname=aname,
pw=pw,
pw=html_sh_esc(pw),
)
self.reply(html.encode("utf-8"))
return True
@@ -4754,7 +4983,6 @@ class HttpCli(object):
if zi == 2 or (zi == 1 and self.avol):
dl_list = self.get_dls()
for t0, t1, sent, sz, vp, dl_id, uname in dl_list:
rem = sz - sent
td = max(0.1, now - t0)
rd, fn = vsplit(vp)
if not rd:
@@ -4774,6 +5002,11 @@ class HttpCli(object):
fn = html_escape(fn) if fn else self.conn.hsrv.iiam
dls.append((perc, hsent, spd, eta, idle, usr, erd, rds, fn))
if self.args.have_unlistc:
allvols = self.asrv.vfs.all_vols
rvol = [x for x in rvol if "unlistcr" not in allvols[x[1:-1]].flags]
wvol = [x for x in wvol if "unlistcw" not in allvols[x[1:-1]].flags]
fmt = self.uparam.get("ls", "")
if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
fmt = "v"
@@ -4846,7 +5079,7 @@ class HttpCli(object):
def setck(self) -> bool:
k, v = self.uparam["setck"].split("=", 1)
t = 0 if v in ("", "x") else 86400 * 299
ck = gencookie(k, v, self.args.R, False, t)
ck = gencookie(k, v, self.args.R, self.args.cookie_lax, False, t)
self.out_headerlist.append(("Set-Cookie", ck))
if "cc" in self.ouparam:
self.redirect("", "?h#cc")
@@ -4858,7 +5091,7 @@ class HttpCli(object):
for k in ALL_COOKIES:
if k not in self.cookies:
continue
cookie = gencookie(k, "x", self.args.R, False)
cookie = gencookie(k, "x", self.args.R, self.args.cookie_lax, False)
self.out_headerlist.append(("Set-Cookie", cookie))
self.redirect("", "?h#cc")
@@ -4928,15 +5161,24 @@ class HttpCli(object):
return "" # unhandled / fallthrough
def scanvol(self) -> bool:
if not self.can_admin:
raise Pebkac(403, "'scanvol' not allowed for user " + self.uname)
if self.args.no_rescan:
raise Pebkac(403, "the rescan feature is disabled in server config")
vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True)
vpaths = self.uparam["scan"].split(",/")
if vpaths == [""]:
vpaths = [self.vpath]
args = [self.asrv.vfs.all_vols, [vn.vpath], False, True]
vols = []
for vpath in vpaths:
vn, _ = self.asrv.vfs.get(vpath, self.uname, True, True)
vols.append(vn.vpath)
if self.uname not in vn.axs.uadmin:
self.log("rejected scanning [%s] => [%s];" % (vpath, vn.vpath), 3)
raise Pebkac(403, "'scanvol' not allowed for user " + self.uname)
self.log("trying to rescan %d volumes: %r" % (len(vols), vols))
args = [self.asrv.vfs.all_vols, vols, False, True]
x = self.conn.hsrv.broker.ask("up2k.rescan", *args)
err = x.get()
@@ -5352,7 +5594,33 @@ class HttpCli(object):
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True
html = self.j2s("rups", this=self, v=jtxt)
html = self.j2s("rups", this=self, v=json_hesc(jtxt))
self.reply(html.encode("utf-8"), status=200)
return True
def tx_idp(self) -> bool:
if self.uname.lower() not in self.args.idp_adm_set:
raise Pebkac(403, "'idp' not allowed for user " + self.uname)
cmd = self.uparam["idp"]
if cmd.startswith("rm="):
import sqlite3
db = sqlite3.connect(self.args.idp_db)
db.execute("delete from us where un=?", (cmd[3:],))
db.commit()
db.close()
self.conn.hsrv.broker.ask("reload", False, True).get()
self.redirect("", "?idp")
return True
rows = [
[k, "[%s]" % ("], [".join(v))]
for k, v in sorted(self.asrv.idp_accs.items())
]
html = self.j2s("idp", this=self, rows=rows, now=int(time.time()))
self.reply(html.encode("utf-8"), status=200)
return True
@@ -5390,15 +5658,15 @@ class HttpCli(object):
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
skey = self.uparam.get("skey") or self.vpath.split("/")[-1]
if self.args.shr_v:
self.log("handle_eshare: " + self.req)
self.log("handle_eshare: " + skey)
cur = idx.get_shr()
if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...")
skey = self.vpath.split("/")[-1]
rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
un = rows[0][0] if rows and rows[0] else ""
@@ -5427,10 +5695,10 @@ class HttpCli(object):
cur.connection.commit()
if reload:
self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("reload", False, True).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
self.redirect(self.args.SRS + "?shares")
self.redirect("", "?shares")
return True
def handle_share(self, req: dict[str, str]) -> bool:
@@ -5504,6 +5772,7 @@ class HttpCli(object):
raise Pebkac(400, "selected file not found on disk: [%s]" % (fn,))
pw = req.get("pw") or ""
pw = self.asrv.ah.hash(pw)
now = int(time.time())
sexp = req["exp"]
exp = int(sexp) if sexp else 0
@@ -5518,7 +5787,7 @@ class HttpCli(object):
cur.execute(q, (skey, fn))
cur.connection.commit()
self.conn.hsrv.broker.ask("reload", False, False).get()
self.conn.hsrv.broker.ask("reload", False, True).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
fn = quotep(fns[0]) if len(fns) == 1 else ""
@@ -5906,13 +6175,13 @@ class HttpCli(object):
self.log("#wow #whoa")
if not self.args.nid:
free, total, _ = get_df(abspath, False)
if total is not None:
free, total, zs = get_df(abspath, False)
if total:
h1 = humansize(free or 0)
h2 = humansize(total)
srv_info.append("{} free of {}".format(h1, h2))
elif free is not None:
srv_info.append(humansize(free, True) + " free")
elif zs:
self.log("diskfree(%r): %s" % (abspath, zs), 3)
srv_infot = "</span> // <span>".join(srv_info)
@@ -6121,7 +6390,7 @@ class HttpCli(object):
margin = "-"
sz = inf.st_size
zd = datetime.fromtimestamp(linf.st_mtime, UTC)
zd = datetime.fromtimestamp(max(0, linf.st_mtime), UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year,
zd.month,

View File

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

View File

@@ -123,6 +123,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 +176,7 @@ class HttpSrv(object):
"browser",
"browser2",
"cf",
"idp",
"md",
"mde",
"msg",
@@ -313,6 +315,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")
@@ -394,6 +398,19 @@ class HttpSrv(object):
self.log(self.name, "accept({}): {}".format(fno, ex), c=6)
time.sleep(0.02)
continue
except TypeError as ex:
# on macOS, accept() may return a None saddr if blocked by LittleSnitch;
# unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable
if tcp and not saddr:
t = "accept(%s): failed to accept connection from client due to firewall or network issue"
self.log(self.name, t % (fno,), c=3)
try:
sck.close() # type: ignore
except:
pass
time.sleep(0.02)
continue
raise
if self.args.log_conn:
t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(

View File

@@ -166,12 +166,13 @@ def au_unpk(
znil = [x for x in znil if "cover" in x[0]] or znil
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
t = "cbz: %d files, %d hits" % (nf, len(znil))
using = sorted(znil)[0][1].filename
if znil:
t += ", using " + znil[0][1].filename
t += ", using " + using
log(t)
if not znil:
raise Exception("no images inside cbz")
fi = zf.open(znil[0][1])
fi = zf.open(using)
else:
raise Exception("unknown compression %s" % (pk,))

View File

@@ -320,7 +320,7 @@ class SMB(object):
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
try:
bos.makedirs(ap2)
bos.makedirs(ap2, vf=vfs2.flags)
except:
pass
@@ -334,7 +334,7 @@ class SMB(object):
t = "blocked mkdir (no-write-acc %s): /%s @%s"
yeet(t % (vfs.axs.uwrite, vpath, uname))
return bos.mkdir(ap)
return bos.mkdir(ap, vfs.flags["chmod_d"])
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
try:

View File

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

View File

@@ -27,6 +27,7 @@ if True: # pylint: disable=using-constant-test
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
from .authsrv import BAD_CFG, AuthSrv
from .bos import bos
from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
from .pwhash import HAVE_ARGON2
@@ -50,6 +51,7 @@ from .util import (
HAVE_PSUTIL,
HAVE_SQLITE3,
HAVE_ZMQ,
RE_ANSI,
URL_BUG,
UTC,
VERSIONS,
@@ -59,7 +61,6 @@ from .util import (
HMaccas,
ODict,
alltrace,
ansi_re,
build_netmap,
expat_ver,
gzip,
@@ -88,6 +89,7 @@ if PY2:
range = xrange # type: ignore
VER_IDP_DB = 1
VER_SESSION_DB = 1
VER_SHARES_DB = 2
@@ -166,6 +168,7 @@ class SvcHub(object):
# for non-http clients (ftp, tftp)
self.bans: dict[str, int] = {}
self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False)
@@ -258,11 +261,15 @@ class SvcHub(object):
self.log("root", "effective %s is %s" % (zs, getattr(args, zs)))
if args.ah_cli or args.ah_gen:
args.idp_store = 0
args.no_ses = True
args.shr = ""
if args.idp_store and args.idp_h_usr:
self.setup_db("idp")
if not self.args.no_ses:
self.setup_session_db()
self.setup_db("ses")
args.shr1 = ""
if args.shr:
@@ -421,26 +428,58 @@ class SvcHub(object):
except:
pass
def setup_session_db(self) -> None:
def _db_onfail_ses(self) -> None:
self.args.no_ses = True
def _db_onfail_idp(self) -> None:
self.args.idp_store = 0
def setup_db(self, which: str) -> None:
"""
the "non-mission-critical" databases; if something looks broken then just nuke it
"""
if which == "ses":
native_ver = VER_SESSION_DB
db_path = self.args.ses_db
desc = "sessions-db"
pathopt = "ses-db"
sanchk_q = "select count(*) from us"
createfun = self._create_session_db
failfun = self._db_onfail_ses
elif which == "idp":
native_ver = VER_IDP_DB
db_path = self.args.idp_db
desc = "idp-db"
pathopt = "idp-db"
sanchk_q = "select count(*) from us"
createfun = self._create_idp_db
failfun = self._db_onfail_idp
else:
raise Exception("unknown cachetype")
if not db_path.endswith(".db"):
zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db"
self.log("root", zs % (pathopt, desc, db_path), 1)
raise Exception(BAD_CFG)
if not HAVE_SQLITE3:
self.args.no_ses = True
t = "WARNING: sqlite3 not available; disabling sessions, will use plaintext passwords in cookies"
self.log("root", t, 3)
failfun()
if which == "ses":
zs = "disabling sessions, will use plaintext passwords in cookies"
elif which == "idp":
zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart"
self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3)
return
assert sqlite3 # type: ignore # !rm
# policy:
# the sessions-db is whatever, if something looks broken then just nuke it
db_path = self.args.ses_db
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
create = True
zs = "creating new" if create else "opening"
self.log("root", "%s sessions-db %s" % (zs, db_path))
self.log("root", "%s %s %s" % (zs, desc, db_path))
for tries in range(2):
sver = 0
@@ -450,17 +489,19 @@ class SvcHub(object):
try:
zs = "select v from kv where k='sver'"
sver = cur.execute(zs).fetchall()[0][0]
if sver > VER_SESSION_DB:
zs = "this version of copyparty only understands session-db v%d and older; the db is v%d"
raise Exception(zs % (VER_SESSION_DB, sver))
if sver > native_ver:
zs = "this version of copyparty only understands %s v%d and older; the db is v%d"
raise Exception(zs % (desc, native_ver, sver))
cur.execute("select count(*) from us").fetchone()
cur.execute(sanchk_q).fetchone()
except:
if sver:
raise
sver = 1
self._create_session_db(cur)
err = self._verify_session_db(cur, sver, db_path)
sver = createfun(cur)
err = self._verify_db(
cur, which, pathopt, db_path, desc, sver, native_ver
)
if err:
tries = 99
self.args.no_ses = True
@@ -468,10 +509,10 @@ class SvcHub(object):
break
except Exception as ex:
if tries or sver > VER_SESSION_DB:
if tries or sver > native_ver:
raise
t = "sessions-db is unusable; deleting and recreating: %r"
self.log("root", t % (ex,), 3)
t = "%s is unusable; deleting and recreating: %r"
self.log("root", t % (desc, ex), 3)
try:
cur.close() # type: ignore
except:
@@ -486,7 +527,7 @@ class SvcHub(object):
pass
os.unlink(db_path)
def _create_session_db(self, cur: "sqlite3.Cursor") -> None:
def _create_session_db(self, cur: "sqlite3.Cursor") -> int:
sch = [
r"create table kv (k text, v int)",
r"create table us (un text, si text, t0 int)",
@@ -499,8 +540,31 @@ class SvcHub(object):
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new sessions-db")
return 1
def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int, db_path: str) -> str:
def _create_idp_db(self, cur: "sqlite3.Cursor") -> int:
sch = [
r"create table kv (k text, v int)",
r"create table us (un text, gs text)",
# username, groups
r"create index us_un on us(un)",
r"insert into kv values ('sver', 1)",
]
for cmd in sch:
cur.execute(cmd)
self.log("root", "created new idp-db")
return 1
def _verify_db(
self,
cur: "sqlite3.Cursor",
which: str,
pathopt: str,
db_path: str,
desc: str,
sver: int,
native_ver: int,
) -> str:
# ensure writable (maybe owned by other user)
db = cur.connection
@@ -512,9 +576,16 @@ class SvcHub(object):
except:
owner = 0
if which == "ses":
cons = "Will now disable sessions and instead use plaintext passwords in cookies."
elif which == "idp":
cons = "Each IdP-volume will not become available until its associated user sends their first request."
else:
raise Exception()
if not lock_file(db_path + ".lock"):
t = "the sessions-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --ses-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now disable sessions and instead use plaintext passwords in cookies."
return t % (db_path, owner)
t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s"
return t % (desc, db_path, owner, pathopt, cons)
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
if owner:
@@ -526,9 +597,9 @@ class SvcHub(object):
for k, v in vars:
cur.execute("insert into kv values(?, ?)", (k, v))
if sver < VER_SESSION_DB:
if sver < native_ver:
cur.execute("delete from kv where k='sver'")
cur.execute("insert into kv values('sver',?)", (VER_SESSION_DB,))
cur.execute("insert into kv values('sver',?)", (native_ver,))
db.commit()
cur.close()
@@ -880,6 +951,12 @@ class SvcHub(object):
vs = os.path.expandvars(os.path.expanduser(vs))
setattr(al, k, vs)
for k in "idp_adm".split(" "):
vs = getattr(al, k)
vsa = [x.strip() for x in vs.split(",")]
vsa = [x.lower() for x in vsa if x]
setattr(al, k + "_set", set(vsa))
zs = "dav_ua1 sus_urls nonsus_urls ua_nodoc ua_nozip"
for k in zs.split(" "):
vs = getattr(al, k)
@@ -1043,7 +1120,7 @@ class SvcHub(object):
fn = sel_fn
try:
os.makedirs(os.path.dirname(fn))
bos.makedirs(os.path.dirname(fn))
except:
pass
@@ -1060,6 +1137,9 @@ class SvcHub(object):
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
if getattr(self.args, "free_umask", False):
os.fchmod(lh.fileno(), 0o644)
argv = [pybin] + self.argv
if hasattr(shlex, "quote"):
argv = [shlex.quote(x) for x in argv]
@@ -1329,9 +1409,9 @@ class SvcHub(object):
if self.no_ansi:
fmt = "%s %-21s %s\n"
if "\033" in msg:
msg = ansi_re.sub("", msg)
msg = RE_ANSI.sub("", msg)
if "\033" in src:
src = ansi_re.sub("", src)
src = RE_ANSI.sub("", src)
elif c:
if isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg)

View File

@@ -282,7 +282,7 @@ 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)
try:

View File

@@ -45,6 +45,7 @@ from .util import (
exclude_dotfiles,
min_ex,
runhook,
set_fperms,
undot,
vjoin,
vsplit,
@@ -284,6 +285,7 @@ class Tftpd(object):
if not ptn or not ptn.match(fn.lower()):
return None
tsdt = datetime.fromtimestamp
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
fsroot, vfs_ls, vfs_virt = vn.ls(
rem,
@@ -296,7 +298,7 @@ class Tftpd(object):
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
real1 = dirs1 + fils1
realt = [(datetime.fromtimestamp(mt, UTC), sz, fn) for mt, sz, fn in real1]
realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1]
reals = [
(
"%04d-%02d-%02d %02d:%02d:%02d"
@@ -386,14 +388,20 @@ 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, [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])

View File

@@ -24,13 +24,13 @@ from .util import (
Cooldown,
Daemon,
afsenc,
atomic_move,
fsenc,
min_ex,
runcmd,
statdir,
ub64enc,
vsplit,
wrename,
wunlink,
)
@@ -96,6 +96,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
@@ -265,7 +269,8 @@ class ThumbSrv(object):
self.log("joined waiting room for %r" % (tpath,))
except:
thdir = os.path.dirname(tpath)
bos.makedirs(os.path.join(thdir, "w"))
chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755
bos.makedirs(os.path.join(thdir, "w"), vf=chmod)
inf_path = os.path.join(thdir, "dir.txt")
if not bos.path.exists(inf_path):
@@ -280,7 +285,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)
@@ -412,7 +417,7 @@ class ThumbSrv(object):
wunlink(self.log, ap_unpk, vn.flags)
try:
wrename(self.log, ttpath, tpath, vn.flags)
atomic_move(self.log, ttpath, tpath, vn.flags)
except Exception as ex:
if not os.path.exists(tpath):
t = "failed to move [%s] to [%s]: %r"
@@ -677,7 +682,7 @@ class ThumbSrv(object):
except:
pass
else:
wrename(self.log, wtpath, tpath, vn.flags)
atomic_move(self.log, wtpath, tpath, vn.flags)
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))

View File

@@ -915,7 +915,8 @@ class Up2k(object):
# only need to protect register_vpath but all in one go feels right
for vol in vols:
try:
bos.makedirs(vol.realpath) # gonna happen at snap anyways
# mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vf=vol.flags)
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
except Exception as ex:
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
@@ -1119,7 +1120,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@@ -1141,6 +1142,20 @@ class Up2k(object):
del fl[k1]
else:
fl[k1] = ",".join(x for x in fl[k1])
if fl["chmod_d"] == int(self.args.chmod_d, 8):
fl.pop("chmod_d")
try:
if fl["chmod_f"] == int(self.args.chmod_f or "-1", 8):
fl.pop("chmod_f")
except:
pass
for k in ("chmod_f", "chmod_d"):
try:
fl[k] = "%o" % (fl[k])
except:
pass
a = [
(ft if v is True else ff if v is False else fv).format(k, str(v))
for k, v in fl.items()
@@ -1364,6 +1379,10 @@ class Up2k(object):
t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem"
self.log(t % (vol.vpath, rtop), 6)
return True, False
if not vol.check_landmarks():
t = "volume /%s at [%s] will not be indexed due to bad landmarks"
self.log(t % (vol.vpath, rtop), 6)
return True, False
n_add, _, _ = self._build_dir(
db,
@@ -2120,11 +2139,12 @@ class Up2k(object):
return -1
w = bw[:-1].decode("ascii")
w16 = w[:16]
with self.mutex:
try:
q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?"
rd, fn, ip, at = cur.execute(q, (w[:16], w)).fetchone()
rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone()
except:
# file modified/deleted since spooling
continue
@@ -2133,8 +2153,12 @@ class Up2k(object):
rd, fn = s3dec(rd, fn)
if "mtp" in flags:
q = "select 1 from mt where w=? and +k='t:mtp' limit 1"
if cur.execute(q, (w16,)).fetchone():
continue
q = "insert into mt values (?,'t:mtp','a')"
cur.execute(q, (w[:16],))
cur.execute(q, (w16,))
abspath = djoin(ptop, rd, fn)
self.pp.msg = "c%d %s" % (nq, abspath)
@@ -2190,7 +2214,7 @@ class Up2k(object):
return tf, -1
if flt == 1:
q = "select w from mt where w = ?"
q = "select 1 from mt where w=? and +k != 't:mtp'"
if c2.execute(q, (row[0][:16],)).fetchone():
continue
@@ -3231,7 +3255,7 @@ class Up2k(object):
if hr.get("reloc"):
x = pathmod(self.vfs, dst, vp, hr["reloc"])
if x:
zvfs = vfs
ud1 = (vfs.vpath, job["prel"], job["name"])
pdir, _, job["name"], (vfs, rem) = x
dst = os.path.join(pdir, job["name"])
job["vcfg"] = vfs.flags
@@ -3239,7 +3263,8 @@ class Up2k(object):
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"], "")
if zvfs.vpath != vfs.vpath:
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4))
job["hash"] = cj["hash"]
self.log("xbu reloc1:%d..." % (depth,), 6)
@@ -3284,7 +3309,7 @@ class Up2k(object):
reg,
"up2k._get_volsize",
)
bos.makedirs(ap2)
bos.makedirs(ap2, vf=vfs.flags)
vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"])
@@ -3391,11 +3416,11 @@ class Up2k(object):
self.log(t % (mts - mtc, mts, mtc, fp))
ow = False
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
if ow:
self.log("replacing existing file at %r" % (fp,))
cur = None
ptop = job["ptop"]
vf = self.flags.get(ptop) or {}
st = bos.stat(fp)
try:
vrel = vjoin(job["prel"], fname)
@@ -3415,8 +3440,13 @@ class Up2k(object):
else:
dip = self.hub.iphash.s(ip)
suffix = "-%.6f-%s" % (ts, dip)
f, ret = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
f, ret = ren_open(
fname,
"wb",
fdir=fdir,
suffix="-%.6f-%s" % (ts, dip),
vf=vf,
)
f.close()
return ret
@@ -3446,6 +3476,8 @@ class Up2k(object):
linked = False
try:
if "reflink" in flags:
raise Exception("reflink")
if not is_mv and not flags.get("dedup"):
raise Exception("dedup is disabled in config")
@@ -3502,7 +3534,8 @@ class Up2k(object):
linked = True
except Exception as ex:
self.log("cannot link; creating copy: " + repr(ex))
if str(ex) != "reflink":
self.log("cannot link; creating copy: " + repr(ex))
if bos.path.isfile(src):
csrc = src
elif fsrc and bos.path.isfile(fsrc):
@@ -4271,7 +4304,7 @@ class Up2k(object):
self.log(t, 1)
raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs))
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
svn_dbv.realpath, srem_dbv
@@ -4447,7 +4480,10 @@ class Up2k(object):
vp = vjoin(dvp, rem)
try:
dvn, drem = self.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem))
dap = dvn.canonical(drem)
bos.mkdir(dap, dvn.flags["chmod_d"])
if "chown" in dvn.flags:
bos.chown(dap, dvn.flags["uid"], dvn.flags["gid"])
except:
pass
@@ -4517,7 +4553,7 @@ class Up2k(object):
is_xvol = svn.realpath != dvn.realpath
bos.makedirs(os.path.dirname(dabs))
bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
if is_dirlink:
dlabs = absreal(sabs)
@@ -4994,14 +5030,15 @@ class Up2k(object):
if hr.get("reloc"):
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
if x:
zvfs = vfs
ud1 = (vfs.vpath, job["prel"], job["name"])
pdir, _, job["name"], (vfs, rem) = x
job["vcfg"] = vf = vfs.flags
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"], "")
if zvfs.vpath != vfs.vpath:
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
self.log("xbu reloc2:%d..." % (depth,), 6)
return self._handle_json(job, depth + 1)
@@ -5023,8 +5060,13 @@ class Up2k(object):
else:
dip = self.hub.iphash.s(job["addr"])
suffix = "-%.6f-%s" % (job["t0"], dip)
f, job["tnam"] = ren_open(tnam, "wb", fdir=pdir, suffix=suffix)
f, job["tnam"] = ren_open(
tnam,
"wb",
fdir=pdir,
suffix="-%.6f-%s" % (job["t0"], dip),
vf=vf,
)
try:
abspath = djoin(pdir, job["tnam"])
sprs = job["sprs"]

View File

@@ -105,6 +105,7 @@ def _ens(want: str) -> tuple[int, ...]:
# WSAENOTSOCK - no longer a socket
# EUNATCH - can't assign requested address (wifi down)
E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET")
E_SCK_WR = _ens("EPIPE ESHUTDOWN EBADFD")
E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
E_ACCESS = _ens("EACCES WSAEACCES")
@@ -153,6 +154,16 @@ try:
except:
HAVE_PSUTIL = False
try:
if os.environ.get("PRTY_NO_MAGIC") or (
ANYWIN and not os.environ.get("PRTY_FORCE_MAGIC")
):
raise Exception()
import magic
except:
pass
if True: # pylint: disable=using-constant-test
import types
from collections.abc import Callable, Iterable
@@ -175,8 +186,6 @@ if True: # pylint: disable=using-constant-test
if TYPE_CHECKING:
import magic
from .authsrv import VFS
from .broker_util import BrokerCli
from .up2k import Up2k
@@ -234,7 +243,18 @@ except:
BITNESS = struct.calcsize("P") * 8
ansi_re = re.compile("\033\\[[^mK]*[mK]")
RE_ANSI = re.compile("\033\\[[^mK]*[mK]")
RE_HTML_SH = re.compile(r"[<>&$?`\"';]")
RE_CTYPE = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
RE_CDISP = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
RE_CDISP_FIELD = re.compile(
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
RE_CDISP_FILE = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
RE_MEMTOTAL = re.compile("^MemTotal:.* kB")
RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@@ -479,11 +499,11 @@ def read_ram() -> tuple[float, float]:
with open("/proc/meminfo", "rb", 0x10000) as f:
zsl = f.read(0x10000).decode("ascii", "replace").split("\n")
p = re.compile("^MemTotal:.* kB")
p = RE_MEMTOTAL
zs = next((x for x in zsl if p.match(x)))
a = int((int(zs.split()[1]) / 0x100000) * 100) / 100
p = re.compile("^MemAvailable:.* kB")
p = RE_MEMAVAIL
zs = next((x for x in zsl if p.match(x)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except:
@@ -1256,8 +1276,6 @@ class Magician(object):
self.magic: Optional["magic.Magic"] = None
def ext(self, fpath: str) -> str:
import magic
try:
if self.bad_magic:
raise Exception()
@@ -1580,6 +1598,8 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None)
vf = kwargs.pop("vf", None)
fperms = vf and "fperms" in vf
if fname == os.devnull:
return fun(fname, *args, **kwargs), fname
@@ -1623,6 +1643,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8"))
if fperms:
set_fperms(f2, vf)
if fperms:
set_fperms(f, vf)
return f, fname
@@ -1684,14 +1709,10 @@ class MultipartParser(object):
self.args = args
self.headers = http_headers
self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
self.re_cdisp = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
self.re_cdisp_field = re.compile(
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
self.re_cdisp_file = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
self.re_ctype = RE_CTYPE
self.re_cdisp = RE_CDISP
self.re_cdisp_field = RE_CDISP_FIELD
self.re_cdisp_file = RE_CDISP_FILE
self.boundary = b""
self.gen: Optional[
@@ -1963,7 +1984,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
return fn
def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
if alg == 1:
zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
else:
@@ -1973,6 +1994,13 @@ def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str
return ub64enc(hashlib.sha512(zb).digest()).decode("ascii")
def _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
return _gen_filekey(alg, salt, fspath.replace("/", "\\"), fsize, inode)
gen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey
def gen_filekey_dbg(
alg: int,
salt: str,
@@ -2019,15 +2047,25 @@ def formatdate(ts: Optional[float] = None) -> str:
return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s)
def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") -> str:
def gencookie(
k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = ""
) -> str:
v = v.replace("%", "%25").replace(";", "%3B")
if dur:
exp = formatdate(time.time() + dur)
else:
exp = "Fri, 15 Aug 1997 01:00:00 GMT"
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax"
return t % (k, v, r, exp, "; Secure" if tls else "", txt)
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s"
return t % (
k,
v,
r,
exp,
"; Secure" if tls else "",
txt,
"Lax" if lax else "Strict",
)
def humansize(sz: float, terse: bool = False) -> str:
@@ -2216,6 +2254,16 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:
return ret
def html_sh_esc(s: str) -> str:
s = re.sub(RE_HTML_SH, "_", s).replace(" ", "%20")
s = s.replace("\r", "_").replace("\n", "_")
return s
def json_hesc(s: str) -> str:
return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str:
"""html.escape but also newlines"""
s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@@ -2396,11 +2444,11 @@ def pathmod(
# try to map abspath to vpath
np = np.replace("/", os.sep)
for vn_ap, vn in vfs.all_aps:
for vn_ap, vns in vfs.all_aps:
if not np.startswith(vn_ap):
continue
zs = np[len(vn_ap) :].replace(os.sep, "/")
nvp = vjoin(vn.vpath, zs)
nvp = vjoin(vns[0].vpath, zs)
break
if nvp == "\n":
@@ -2535,6 +2583,14 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
log("lsof failed; " + min_ex(), 3)
def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> None:
fno = f.fileno()
if "chmod_f" in vf:
os.fchmod(fno, vf["chmod_f"])
if "chown" in vf:
os.fchown(fno, vf["uid"], vf["gid"])
def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool:
@@ -2583,6 +2639,11 @@ def _fs_mvrm(
now = time.time()
if ex.errno == errno.ENOENT:
return False
if not attempt and ex.errno == errno.EXDEV:
t = "using copy+delete (%s)\n %s\n %s"
log(t % (ex.strerror, src, dst))
osfun = shutil.move
continue
if now - t0 > maxtime or attempt == 90209:
raise
if not attempt:
@@ -2607,15 +2668,18 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
elif flags.get("mv_re_t"):
_fs_mvrm(log, src, dst, True, flags)
else:
os.replace(bsrc, bdst)
def wrename(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> bool:
if not flags.get("mv_re_t"):
os.rename(fsenc(src), fsenc(dst))
return True
return _fs_mvrm(log, src, dst, False, flags)
try:
os.replace(bsrc, bdst)
except OSError as ex:
if ex.errno != errno.EXDEV:
raise
t = "using copy+delete (%s);\n %s\n %s"
log(t % (ex.strerror, src, dst))
try:
os.unlink(bdst)
except:
pass
shutil.move(bsrc, bdst)
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
@@ -2626,7 +2690,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
return _fs_mvrm(log, abspath, "", False, flags)
def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str]:
def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
try:
ap = fsenc(abspath)
while prune and not os.path.isdir(ap) and BOS_SEP in ap:
@@ -2637,17 +2701,22 @@ def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str
assert ctypes # type: ignore # !rm
abspath = fsdec(ap)
bfree = ctypes.c_ulonglong(0)
btotal = ctypes.c_ulonglong(0)
bavail = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree)
ctypes.c_wchar_p(abspath),
ctypes.pointer(bavail),
ctypes.pointer(btotal),
ctypes.pointer(bfree),
)
return (bfree.value, None, "")
return (bavail.value, btotal.value, "")
else:
sv = os.statvfs(ap)
free = sv.f_frsize * sv.f_bfree
free = sv.f_frsize * sv.f_bavail
total = sv.f_frsize * sv.f_blocks
return (free, total, "")
except Exception as ex:
return (None, None, repr(ex))
return (0, 0, repr(ex))
if not ANYWIN and not MACOS:
@@ -3144,11 +3213,13 @@ def unescape_cookie(orig: str) -> str:
return "".join(ret)
def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
def guess_mime(
url: str, path: str = "", fallback: str = "application/octet-stream"
) -> str:
try:
ext = url.rsplit(".", 1)[1].lower()
except:
return fallback
ext = ""
ret = MIMES.get(ext)
@@ -3156,6 +3227,16 @@ def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
x = mimetypes.guess_type(url)
ret = "application/{}".format(x[1]) if x[1] else x[0]
if not ret and path:
try:
with open(fsenc(path), "rb", 0) as f:
ret = magic.from_buffer(f.read(4096), mime=True)
if ret.startswith("text/htm"):
# avoid serving up HTML content unless there was actually a .html extension
ret = "text/plain"
except Exception as ex:
pass
if not ret:
ret = fallback
@@ -4116,7 +4197,12 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream)
return stream
return open(os.path.join(E.mod, name), mode, encoding=enc)
ap = os.path.join(E.mod, name)
if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore
return open(ap, mode, encoding=enc)
class Pebkac(Exception):

View File

@@ -592,9 +592,7 @@ window.baguetteBox = (function () {
preloadPrev(currentIndex);
});
clmod(ebi('bbox-btns'), 'off');
clmod(btnPrev, 'off');
clmod(btnNext, 'off');
show_buttons(0);
updateOffset();
overlay.style.display = 'block';
@@ -776,6 +774,8 @@ window.baguetteBox = (function () {
if (is_vid) {
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
image.setAttribute('controls', 'controls');
image.setAttribute('playsinline', '1');
// ios ignores poster
image.onended = vidEnd;
image.onplay = function () { show_buttons(1); };
image.onpause = function () { show_buttons(); };

View File

@@ -4,6 +4,8 @@
--grid-sz: 10em;
--grid-ln: 3;
--nav-sz: 16em;
--sbw: 0.5em;
--sbh: 0.5em;
--fg: #ccc;
--fg-max: #fff;
@@ -1160,8 +1162,8 @@ html.y #widget.open {
border: 1px solid var(--bg-u5);
border-width: 0 .1em 0 0;
}
#wfm.act+#wzip,
#wfm.act+#wzip+#wnp {
#wfm.act+#wzip1+#wzip,
#wfm.act+#wzip1+#wzip+#wnp {
margin-left: .2em;
padding-left: .2em;
border-left-width: .1em;
@@ -1179,12 +1181,14 @@ html.y #widget.open {
#wtoggle.np #wnp {
display: inline-block;
}
#wtoggle.sel #wzip1,
#wtoggle.sel.np #wnp {
display: none;
}
#wfm a,
#wnp a,
#wm3u a,
#zip1,
#wzip a {
font-size: .5em;
padding: 0 .3em;
@@ -1192,6 +1196,9 @@ html.y #widget.open {
position: relative;
display: inline-block;
}
#zip1 {
font-size: .38em;
}
#wm3u a {
margin: -.2em .1em;
font-size: .45em;
@@ -1205,10 +1212,14 @@ html.y #widget.open {
}
#wfm span,
#wm3u span,
#zip1 span,
#wnp span {
font-size: .6em;
display: block;
}
#zip1 span {
font-size: .9em;
}
#wnp span {
font-size: .7em;
}
@@ -1549,8 +1560,8 @@ html {
z-index: 1;
position: fixed;
background: var(--tree-bg);
left: -.98em;
width: calc(var(--nav-sz) - 0.5em);
left: -.96em;
width: calc(.3em + var(--nav-sz) - var(--sbw));
border-bottom: 1px solid var(--bg-u5);
overflow: hidden;
}
@@ -1816,10 +1827,11 @@ html.y #tree.nowrap .ntree a+a:hover {
line-height: 2.3em;
margin-bottom: 1.5em;
}
#hdoc,
#ghead {
position: sticky;
top: -.3em;
z-index: 1;
z-index: 2;
}
.ghead .btn {
position: relative;
@@ -1829,6 +1841,13 @@ html.y #tree.nowrap .ntree a+a:hover {
white-space: pre;
padding-left: .3em;
}
#tailbtns {
display: none;
}
#taildoc.on+#tailbtns {
display: inherit;
display: unset;
}
#op_unpost {
padding: 1em;
}
@@ -1925,6 +1944,9 @@ html.y #tree.nowrap .ntree a+a:hover {
padding: 1em 0 1em 0;
border-radius: .3em;
}
#doc.wrap {
white-space: pre-wrap;
}
html.y #doc {
box-shadow: 0 0 .3em var(--bg-u5);
background: #f7f7f7;
@@ -2013,6 +2035,9 @@ a.btn,
font-family: 'scp', monospace, monospace;
font-family: var(--font-mono), 'scp', monospace, monospace;
}
#hkhelp b {
text-shadow: 1px 0 0 var(--fg), -1px 0 0 var(--fg), 0 -1px 0 var(--fg);
}
html.noscroll,
html.noscroll .sbar {
scrollbar-width: none;
@@ -2174,18 +2199,25 @@ html.y #bbox-overlay figcaption a {
top: calc(50% - 30px);
width: 44px;
height: 60px;
transition: background-color .3s ease, color .3s ease, left .3s ease, right .3s ease;
}
#bbox-btns button {
transition: background-color .3s ease, color .3s ease;
}
#bbox-btns {
transition: top .3s ease;
}
.bbox-btn {
position: fixed;
}
.bbox-btn,
#bbox-btns {
opacity: 1;
animation: opacity .2s infinite ease-in-out;
#bbox-next.off {
right: -2.6em;
}
#bbox-prev.off {
left: -2.6em;
}
.bbox-btn.off,
#bbox-btns.off {
opacity: 0;
top: -2.2em;
}
#bbox-overlay button {
cursor: pointer;
@@ -2196,8 +2228,6 @@ html.y #bbox-overlay figcaption a {
border-radius: 15%;
background: rgba(50, 50, 50, 0.5);
color: rgba(255,255,255,0.7);
transition: background-color .3s ease;
transition: color .3s ease;
font-size: 1.4em;
line-height: 1.4em;
vertical-align: top;
@@ -3046,7 +3076,8 @@ html.b .ntree a {
padding: .6em .2em;
}
html.b #treepar {
margin-left: .62em;
margin-left: .63em;
width: calc(.1em + var(--nav-sz) - var(--sbw));
border-bottom: .2em solid var(--f-h-b1);
}
html.b #wrap {
@@ -3218,7 +3249,7 @@ html.d #treepar {
#ggrid>a>span {
text-align: center;
padding: 0.2em;
padding: .2em .2em .15em .2em;
}
}
@@ -3241,4 +3272,9 @@ html.d #treepar {
.dropdesc>div>div {
transition: none;
}
#bbox-next,
#bbox-prev,
#bbox-btns {
transition: background-color .3s ease, color .3s ease;
}
}

View File

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

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -1,9 +1,11 @@
var SRS = SR.trimEnd('/') + '/';
var t = QSA('a[k]');
for (var a = 0; a < t.length; a++)
t[a].onclick = rm;
function rm() {
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm',
var u = SRS + '?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')),
xhr = new XHR();
xhr.open('POST', u, true);
@@ -13,7 +15,7 @@ function rm() {
function bump() {
var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'),
u = SR + shr + uricom_enc(k) + '?eshare=' + this.value,
u = SRS + '?skey=' + uricom_enc(k) + '&eshare=' + this.value,
xhr = new XHR();
xhr.open('POST', u, true);

View File

@@ -135,6 +135,10 @@
<h1 id="cc">other stuff:</h1>
<ul>
{%- if this.uname in this.args.idp_adm_set %}
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{% endif %}
{%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
{% endif %}

View File

@@ -39,6 +39,7 @@ var Ls = {
"ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!",
"ae1": "utgående:",
"af1": "vis nylig opplastede filer",
"ag1": "vis kjente IdP-brukere",
},
"eng": {
"d2": "shows the state of all active threads",
@@ -90,7 +91,50 @@ var Ls = {
"ad1": "启用 no304 将禁用所有缓存;如果 k304 不够,可以尝试此选项。这将消耗大量的网络流量!", //m
"ae1": "正在下载:", //m
"af1": "显示最近上传的文件", //m
}
"ag1": "查看已知 IdP 用户", //m
},
"deu": {
"a1": "Neu laden",
"b1": "Tach, wie geht's? &nbsp; <small>(Du bist nicht angemeldet)</small>",
"c1": "Abmelden",
"d1": "Zustand", // TLNote: "d2" is the tooltip for this button
"d2": "Zeigt den Zustand aller aktiven Threads",
"e1": "Config neu laden",
"e2": "Konfigurationsdatei neu laden (Accounts/Volumes/VolFlags)$Nund scannt alle e2ds-Volumes$N$NBeachte: Jegliche Änderung an globalen Einstellungen$Nbenötigt einen Neustart zum Anwenden",
"f1": "Du kannst lesen:",
"g1": "Du kannst hochladen nach:",
"cc1": "Andere Dinge:",
"h1": "k304 deaktivieren", // TLNote: "j1" explains what k304 is
"i1": "k304 aktivieren",
"j1": "k304 trennt die Clientverbindung bei jedem HTTP 304, was Bugs mit problematischen Proxies vorbeugen kann (z.B. nicht ladenden Seiten), macht Dinge aber generell langsamer",
"k1": "Client-Einstellungen zurücksetzen",
"l1": "Melde dich an für mehr:",
"m1": "Willkommen zurück,", // TLNote: "welcome back, USERNAME"
"n1": "404 Nicht gefunden &nbsp;┐( ´ -`)┌",
"o1": 'or maybe you don\'t have access -- try a password or <a href="' + SR + '/?h">go home</a>',
"p1": "403 Verboten &nbsp;~┻━┻",
"q1": 'Benutze ein Passwort oder <a href="' + SR + '/?h">gehe zur Homepage</a>',
"r1": "Gehe zur Homepage",
".s1": "Neu scannen",
"t1": "Aktion", // TLNote: this is the header above the "rescan" buttons
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v1": "Verbinden",
"v2": "Benutze diesen Server als lokale Festplatte",
"w1": "Zu HTTPS wechseln",
"x1": "Passwort ändern",
"y1": "Shares bearbeiten", // TLNote: shows the list of folders that the user has decided to share
"z1": "Share entsperren:", // TLNote: the password prompt to see a hidden share
"ta1": "Trage zuerst dein Passwort ein",
"ta2": "Wiederhole dein Passwort zur Bestätigung:",
"ta3": "Da stimmt etwas nicht; probier's nochmal",
"aa1": "Eingehende Dateien:",
"ab1": "no304 deaktivieren",
"ac1": "no304 aktivieren",
"ad1": "Das Aktivieren von no304 deaktiviert jegliche Form von Caching; probier dies, wenn k304 nicht genug war. Dies verschwendet eine grosse Menge Netzwerk-Traffic!",
"ae1": "Aktive Downloads:",
"af1": "Zeige neue Uploads",
},
};
if (window.langmod)

View File

@@ -36,7 +36,7 @@
<span class="os lin mac">
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</span>
<a href="#" id="setpw">use real password</a>
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
</p>
@@ -101,6 +101,7 @@
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
{%- endif %}
</pre>
<p>on KDE Dolphin, use <code>webdav{{ s }}://{{ ep }}/{{ rvp }}</code></p>
</div>
<div class="os mac">
@@ -239,14 +240,26 @@
<div class="os win">
<h1>ShareX</h1>
<p>to upload screenshots using ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> or <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p>
<p>to upload screenshots using ShareX <a href="https://getsharex.com/">v15+</a>, save this as <code>copyparty.sxcu</code> and run it:</p>
<pre class="dl" name="copyparty.sxcu">
{ "Version": "15.0.0", "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"Body": "MultipartFormData", "URL": "{response}",
"RequestMethod": "POST", "FileFormName": "f" }
</pre>
<p>for ShareX <a href="https://github.com/ShareX/ShareX/releases/tag/v12.1.1">v12</a> specifically, save this as <code>copyparty.sxcu</code> and run it:</p>
<pre class="dl" name="copyparty.sxcu">
{ "Name": "copyparty",
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
"Headers": {
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
"accept": "url"
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
},
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"FileFormName": "f" }

View File

@@ -49,7 +49,7 @@ function setos(os) {
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
ebi('setpw').onclick = function (e) {
function setpw(e) {
ev(e);
modal.prompt('password:', '', function (v) {
if (!v)
@@ -57,7 +57,7 @@ ebi('setpw').onclick = function (e) {
var pw0 = ebi('pw0').innerHTML,
oa = QSA('b');
for (var a = 0; a < oa.length; a++)
if (oa[a].innerHTML == pw0)
oa[a].textContent = v;
@@ -65,3 +65,5 @@ ebi('setpw').onclick = function (e) {
add_dls();
});
}
if (ebi('setpw'))
ebi('setpw').onclick = setpw;

View File

@@ -1,6 +1,18 @@
"use strict";
(function () {
var x = sread('nosubtle');
if (x === '0' || x === '1')
nosubtle = parseInt(x);
if ((nosubtle > 1 && !CHROME && !FIREFOX) ||
(nosubtle > 2 && !CHROME) ||
(CHROME && nosubtle > VCHROME) ||
!WebAssembly)
nosubtle = 0;
})();
function goto_up2k() {
if (up2k === false)
return goto('bup');
@@ -23,7 +35,7 @@ var up2k = null,
m = 'will use ' + sha_js + ' instead of native sha512 due to';
try {
if (sread('nosubtle') || window.nosubtle)
if (nosubtle)
throw 'chickenbit';
var cf = crypto.subtle || crypto.webkitSubtle;
cf.digest('SHA-512', new Uint8Array(1)).then(
@@ -825,7 +837,7 @@ function up2k_init(subtle) {
}
qsr('#u2depmsg');
var o = mknod('div', 'u2depmsg');
o.innerHTML = m;
o.innerHTML = nosubtle ? '' : m;
ebi('u2foot').appendChild(o);
}
loading_deps = true;
@@ -881,7 +893,8 @@ function up2k_init(subtle) {
bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo);
bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null);
bcfg_bind(uc, 'az', 'u2sort', u2sort.indexOf('n') + 1, set_u2sort);
bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME), set_hashw);
bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME || VCHROME > 136), set_hashw);
bcfg_bind(uc, 'hwasm', 'nosubtle', nosubtle, set_nosubtle);
bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag);
bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx);
@@ -1442,9 +1455,16 @@ function up2k_init(subtle) {
if (CHROME) {
// chrome-bug 383568268 // #124
nw = Math.max(1, (nw > 4 ? 4 : (nw - 1)));
if (VCHROME < 137)
nw = (subtle && !MOBILE && nw > 2) ? 2 : nw;
}
var x = sread('u2hashers') || window.u2hashers;
if (x) {
console.log('u2hashers is overriding default-value ' + nw);
nw = parseInt(x);
}
for (var a = 0; a < nw; a++)
hws.push(new Worker(SR + '/.cpr/w.hash.js?_=' + TS));
@@ -2213,6 +2233,7 @@ function up2k_init(subtle) {
reading = 0,
max_readers = 1,
opt_readers = 2,
failed = false,
free = [],
busy = {},
nbusy = 0,
@@ -2262,6 +2283,14 @@ function up2k_init(subtle) {
tasker();
}
function go_fail() {
failed = true;
if (nbusy)
return;
apop(st.busy.hash, t);
st.bytes.finished += t.size;
}
function onmsg(d) {
d = d.data;
var k = d[0];
@@ -2276,6 +2305,12 @@ function up2k_init(subtle) {
return vis_exh(d[1], 'up2k.js', '', '', d[1]);
if (k == "fail") {
var nchunk = d[1];
free.push(busy[nchunk]);
delete busy[nchunk];
nbusy--;
reading--;
pvis.seth(t.n, 1, d[1]);
pvis.seth(t.n, 2, d[2]);
console.log(d[1], d[2]);
@@ -2283,9 +2318,7 @@ function up2k_init(subtle) {
got_oserr();
pvis.move(t.n, 'ng');
apop(st.busy.hash, t);
st.bytes.finished += t.size;
return;
return go_fail();
}
if (k == "ferr")
@@ -2318,6 +2351,9 @@ function up2k_init(subtle) {
t.hash.push(nchunk);
pvis.hashed(t);
if (failed)
return go_fail();
if (t.hash.length < nchunks)
return nbusy < opt_readers && go_next();
@@ -2395,8 +2431,8 @@ function up2k_init(subtle) {
try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); }
};
xhr.timeout = 34000;
xhr.open('HEAD', t.purl + uricom_enc(t.name), true);
xhr.timeout = 34000;
xhr.send();
}
@@ -2875,7 +2911,8 @@ function up2k_init(subtle) {
st.bytes.inflight += db;
xhr.bsent = nb;
xhr.timeout = 64000 + Date.now() - xhr.t0;
if (!IE)
xhr.timeout = 64000 + Date.now() - xhr.t0;
pvis.prog(t, pcar, nb);
};
xhr.onload = function (xev) {
@@ -2923,7 +2960,7 @@ function up2k_init(subtle) {
xhr.bsent = 0;
xhr.t0 = Date.now();
xhr.timeout = 42000;
xhr.timeout = 1000 * (IE ? 1234 : 42);
xhr.responseType = 'text';
xhr.send(t.fobj.slice(car, cdr));
}
@@ -3269,6 +3306,12 @@ function up2k_init(subtle) {
}
}
function set_nosubtle(v) {
if (!WebAssembly)
return toast.err(10, L.u_nowork);
modal.confirm(L.lang_set, location.reload.bind(location), null);
}
function set_upnag(en) {
function nopenag() {
bcfg_set('upnag', uc.upnag = false);

View File

@@ -32,7 +32,7 @@ var wah = '',
CHROME = !!window.chrome, // safari=false
VCHROME = CHROME ? 1 : 0,
UA = '' + navigator.userAgent,
IE = /Trident\//.test(UA),
IE = !!document.documentMode,
FIREFOX = ('netscape' in window) && / rv:/.test(UA),
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(UA),
LINUX = /Linux/.test(UA),
@@ -69,7 +69,7 @@ try {
CHROME = navigator.userAgentData.brands.find(function (d) { return d.brand == 'Chromium' });
if (CHROME)
VCHROME = CHROME.version;
VCHROME = parseInt(CHROME.version);
else
VCHROME = 0;
@@ -183,7 +183,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
if (url.indexOf(' > eval') + 1 && !evalex_fatal)
return; // md timer
if (IE && url.indexOf('prism.js') + 1)
if (url.indexOf('prism.js') + 1)
return;
if (url.indexOf('easymde.js') + 1)
@@ -383,8 +383,10 @@ if (!String.prototype.format)
});
};
var have_URL = false;
try {
new URL('/a/', 'https://a.com/');
have_URL = true;
}
catch (ex) {
console.log('ie11 shim URL()');
@@ -732,6 +734,16 @@ function makeSortable(table, cb) {
}
function assert_vp(path) {
if (path.indexOf('//') + 1)
throw 'nonlocal1: ' + path;
var o = window.location.origin;
if (have_URL && (new URL(path, o)).origin != o)
throw 'nonlocal2: ' + path;
}
function linksplit(rp, id) {
var ret = [],
apath = '/',
@@ -1229,7 +1241,7 @@ function dl_file(url) {
function cliptxt(txt, ok) {
var fb = function () {
console.log('clip-fb');
var o = mknod('input');
var o = mknod('textarea');
o.value = txt;
document.body.appendChild(o);
o.focus();
@@ -1239,6 +1251,8 @@ function cliptxt(txt, ok) {
ok();
};
try {
if (!window.isSecureContext)
throw 1;
navigator.clipboard.writeText(txt).then(ok, fb);
}
catch (ex) { fb(); }

View File

@@ -4,6 +4,16 @@
function hex2u8(txt) {
return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); }));
}
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
return {
'&': '&amp;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;'
}[c];
});
}
var subtle = null;
@@ -19,6 +29,8 @@ catch (ex) {
}
function load_fb() {
subtle = null;
if (self.hashwasm)
return;
importScripts('deps/sha512.hw.js');
console.log('using fallback hasher');
}

View File

@@ -1,3 +1,240 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0727-2305 `v1.18.5` SECURITY: fix XSS in media tags
## ⚠️ ATTN: this release fixes an XSS vulnerability
[GHSA-9q4r-x2hj-jmvr](https://github.com/9001/copyparty/security/advisories/GHSA-9q4r-x2hj-jmvr), exploitable in two different ways, could let an attacker execute arbitrary javascript on other users:
* either: tricking someone into clicking a malicious URL to load and execute javascript
* or: uploading a malicious audio file to the server, affecting any successive visitors
so, with new and curious eyes on the project, we are starting off with a bang. Huge thanks to @altperfect for finding and reporting this earlier today.
## recent important news
* [v1.18.5 (2025-07-28)](https://github.com/9001/copyparty/releases/tag/v1.18.5) fixed XSS in display of media tags
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
## 🧪 new features
* #214 option to stop playback after one song, and/or at end of folder 6bb27e60
## 🩹 bugfixes
* GHSA-9q4r-x2hj-jmvr 895880ae
* block external m3u files 2228f81f
* #202 the connect-page could show IP-address when it should have used hostnames/domains b0dec83a
* scrolling locked after tailing a file and closing it creatively d197e754
## 🔧 other changes
* #189 the `SameSite` cookie parameter now defaults to `Strict`, increasing CSRF protection ca6d0b8d
* new option `--cookie-lax` reverts to previous value `Lax`
* docker: add FTPS support b4199847
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0725-1841 `v1.18.4` Landmarks
## 🧪 new features
* #182 [Landmarks](https://github.com/9001/copyparty#database-location) edba7fff
* detects that a storage backend is glitching out and disengage the up2k-database as a precaution
* #183 quickdelete 21a96bcf
* new togglebutton `qdel` in the UI which reduces the number of deletion confirmations by one
* global-option `--qdel=0` which can bring it all the way to zero (good luck)
## 🩹 bugfixes
* fix unpost in recently created shares 2d322dd4
* fix filekeys on windows df6d4df4
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0721-2307 `v1.18.3` drop the umask
## 🧪 new features
* #181 the default chmod (unix-permissions) of new files and folders can now be changed 9921c43e
* `--chmod-d` or volflag `chmod_d` sets directory permissions; default is 755
* `--chmod-f` or volflag `chmod_f` sets file permissions; default is usually 644 (OS-defined)
* see `--help-chmod` which explains the numbers
## 🩹 bugfixes
* #179 couldn't combine `--shr` (shares) and `--xvol` (symlink-guard) 0f0f8d90
* #180 gallery buttons could still be clicked when faded-out 8c32b0e7
* rss-feeds were slightly busted when combined with rp-loc (location-based proxying) 56d3bcf5
* music-playback within search-results no longer jumps into the next folder at end-of-list 9bc4c5d2
* video-playback on iOS now behaves like on all other platforms 78605d9a
* (it would force-switch into fullscreen because that's their default)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0707-1419 `v1.18.2` idp-vol persistence
## 🧪 new features
* IdP-volumes can optionally be persisted across restarts d162502c
* there is a UI to manage the cached users/groups 4f264a0a
* only available to users listed in the new option `--idp-adm`
* api for manually rescanning several volumes at once 42c199e7
* `/some/path/?scan` does that one volume like before
* `/any/path/?scan=/vol1,/another/vol2` rescans `/vol1` and `/another/vol2`
* volflag to hide volume from listing in controlpanel fd7c71d6
## 🩹 bugfixes
* macos: fix confusing crash when blocked by [Little Snitch](https://www.obdev.at/products/littlesnitch/) bf11b2a4
* unpost could break in some hairy reverseproxy setups 1b2d3985
* copyparty32.exe: fix segfault on win7 c9fafb20
* ui: fix navpane overlapping the scrollbar (still a bit jank but eh) 7ef6fd13
* usb-eject: support all volume names ed908b98
* docker: ensure clean slate deb6711b
* fix up2k on ie11 d2714434
## 🔧 other changes
* update buildscript for keyfinder to support llvm 65c4e035
* #175 add `python-magic` into the `iv` and `dj` docker flavors (thx @Morganamilo) 77274e9d
* properly killed the experimental docker flavors to avoid confusion 8306e3d9
* copyparty.exe: updated pillow 299cff3f f6be3905
* avif support was removed to save 2 MiB
## 🌠 fun facts
* this release was slightly delayed due to a [norwegian traffic jam](https://a.ocv.me/pub/g/2025/07/PXL_20250706_143558381.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0622-0020 `v1.18.0` Logtail
## 🧪 new features
* textfile-viewer can now livestream logfiles (and other growing files) 17fa4906 77df17d1 a1c7a095 6ecf4fdc
* see [readme](https://github.com/9001/copyparty/#textfile-viewer) and the [live demo](https://a.ocv.me/pub/demo/logtail/)
* IdP-volumes: extend syntax for excluding certain users/groups 2e53f797
* the commit-message explains it well enough
* new option `--see-dots` to show dotfiles in the web-ui by default c599e2aa
* #171 automatic mimetype detection for files without extensions (thx @Morganamilo!) ec05f8cc 9dd5dec0
* default-disabled since it has a performance impact on webdav
* there are plans to fix this by using the db instead
* #170 improve custom filetype icons
* be less strict; if a thumbnail is set for `.gz` files, use it for `.tar.gz` too c75b0c25
* improve config docs fa5845ff
## 🩹 bugfixes
* cosmetic: get rid of some noise along the bottom of some cards in the gridview 8cae7a71
* cosmetic: satisfy a new syntax warning in cpython-3.14 5ac38648
## 🔧 other changes
* properly document how to [build from source](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-scratch) / build from scratch f61511d8
* update deps
* copyparty.exe: python 3.13 1eff87c3
* webdeps: dompurify 7eca90cc
## 🌠 fun facts
* this release was cooked up in a [swedish forest cabin](https://a.ocv.me/pub/g/nerd-stuff/forestparty.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0527-1939 `v1.17.2` pushing chrome to the limits (and then some)
## 🧪 new features
* not this time
## 🩹 bugfixes
* up2k: improve file-hashing speed on recent versions of google chrome e3e51fb8
* speed increased from 319 to 513 MiB/s by default (but older chrome versions did 748...)
* read the commit message for the full story, but basically chrome has gotten gradually slower over the past couple versions (starting from v133) and this makes it slightly less bad again
* hashing speed can be further improved from `0.5` to `1.1` GiB/s by enabling the `[wasm]` option in the `[⚙️] settings` tab
* this option can be made default-enabled with `--nosubtle 137` but beware that this increases the chances of running into browser-bugs (foreshadowing...)
* up2k: fix errorhandler for browser-bugs (oom and such) 49c71247
* because [chrome-bug 383568268](https://issues.chromium.org/issues/383568268) is about to make a [surprise return?!](https://issues.chromium.org/issues/383568268#comment14)
* #168 fix uploading into shares if path-based proxying is used 9cb93ae1
* #165 unconditionally heed `--rp-loc` 84f5f417
* the config-option for [path-based proxying](https://github.com/9001/copyparty/#reverse-proxy) was ignored if the reverse-proxy was untrusted; this was confusing and not strictly necessary
## 🔧 other changes
* #166 the nixos module was improved once more (thx @msfjarvis!) 48470f6b 60fb1207
* added usage instructions to [minimal-up2k.js](https://github.com/9001/copyparty/tree/hovudstraum/contrib/plugins#example-browser-js), the up2k-ui [simplifier](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) 1d308eeb
* docker: improve feedback if config is bad or missing 28b63e58
## 🌠 fun facts
* this release was tested using an [unreliable rdp connection](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250526_021207825.jpg) through two ssh-jumphosts to a qemu win10 vm back home from the bergen-oslo night train wifi
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0518-2234 `v1.17.1` as seen on archlinux
## 🧪 new features
* new toolbar button to zip/tar the currently open folder 256dad8c
* new options to specify the default checksum algorithm for PUT/bup/WebDAV uploads 0de09860
* #164 new option `--put-name` to specify the filename of nameless uploads 5dcd88a6
* the default is still `put-TIMESTAMP-IPADDRESS.bin`
## 🩹 bugfixes
* #162 password-protected shares was incompatible with password-hashing c3ef3fdc
* #161 m3u playlist creation was only possible over https 94352f27
* when relocating/redirecting an upload from an xbu hook (execute-before-upload), could miss an already existing file at the destination and create another copy 0a9a8077
* some edgecases when moving files between filesystems f425ff51
* improve tagscan-resume after a server restart (primarily for dupes) 41fa6b25
* support prehistoric timestamps in fat16 vhd-drives on windows 261236e3
## 🔧 other changes
* #159 the nixos module was improved (thx @gabevenberg and @chinponya!) d1bca1f5
* an archlinux maintainer adopted the aur package; copyparty is now [officially in arch](https://archlinux.org/packages/extra/any/copyparty/) b9ba783c
* #162 add KDE Dolphin instructions to the conect-page d4a8071d
* audioplayer now knows that `.oga` means `.ogg`
## 🌠 fun facts
* this release contains code [pair-programmed during an anime rave](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250503_222654610.jpg)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0426-2149 `v1.17.0` mixtape.m3u
## 🧪 new features
* [m3u playlists](https://github.com/9001/copyparty/#playlists) 897f9d32 ad200f2b 4195762d fff45552
* create and play m3u / m3u8 files
## 🩹 bugfixes
* improve support for ie11 (yes, internet explorer 11) 3090c748 95157d02
* now possible to launch the password-hasher cli while another instance is running dbfc899d
* in preparation of #157 / #159
## 🔧 other changes
* make better decisions when running in a VM with less than 1 GiB RAM dc3b7a27
## 🌠 fun facts
* this release contains code written [less than 1masl](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250425_170037812.jpg) and was gonna be named [hash again](https://www.youtube.com/watch?v=twUFbqyul_M) since it was originally just the password-hasher fix, but then kipun suggested adding playlist support (thx kipun)
* [donations](https://github.com/9001/) are now also possible through github -- good alternative to paypal (y)
* and thanks a lot for the support (and kind words therein) so far, appreciate it :>
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2025-0420-1836 `v1.16.21` unzip-compat

View File

@@ -22,6 +22,7 @@
* [dev env setup](#dev-env-setup)
* [just the sfx](#just-the-sfx)
* [build from release tarball](#build-from-release-tarball) - uses the included prebuilt webdeps
* [build from scratch](#build-from-scratch) - how the sausage is made
* [complete release](#complete-release)
* [debugging](#debugging)
* [music playback halting on phones](#music-playback-halting-on-phones) - mostly fine on android
@@ -190,6 +191,9 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| GET | `?v` | open image/video/audio in mediaplayer |
| GET | `?txt` | get file at URL as plaintext |
| GET | `?txt=iso-8859-1` | ...with specific charset |
| GET | `?tail` | continuously stream a growing file |
| GET | `?tail=1024` | ...starting from byte 1024 |
| GET | `?tail=-128` | ...starting 128 bytes from the end |
| GET | `?th` | get image/video at URL as thumbnail |
| GET | `?th=opus` | convert audio file to 128kbps opus |
| GET | `?th=caf` | ...in the iOS-proprietary container |
@@ -257,6 +261,7 @@ upload modifiers:
|--|--|--|
| GET | `?reload=cfg` | reload config files and rescan volumes |
| GET | `?scan` | initiate a rescan of the volume which provides URL |
| GET | `?scan=/a,/b` | initiate a rescan of volumes `/a` and `/b` |
| GET | `?stack` | show a stacktrace of all threads |
## general
@@ -338,7 +343,7 @@ for the `re`pack to work, first run one of the sfx'es once to unpack it
you need python 3.9 or newer due to type hints
the rest is mostly optional; if you need a working env for vscode or similar
setting up a venv with the below packages is only necessary if you want it for vscode or similar
```sh
python3 -m venv .venv
@@ -350,7 +355,7 @@ pip install mutagen # audio metadata
pip install pyftpdlib # ftp server
pip install partftpy # tftp server
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install Pillow pyheif-pillow-opener # thumbnails
pip install pyvips # faster thumbnails
pip install psutil # better cleanup of stuck metadata parsers on windows
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
@@ -392,6 +397,39 @@ python3 setup.py install --skip-build --prefix=/usr --root=$HOME/pe/copyparty
```
## build from scratch
how the sausage is made:
to get started, first `cd` into the `scripts` folder
* the first step is the webdeps; they end up in `../copyparty/web/deps/` for example `../copyparty/web/deps/marked.js.gz` -- if you need to build the webdeps, run `make -C deps-docker`
* this needs rootless podman and the `podman-docker` compat-layer to pretend it's docker, although it *should* be possible to use rootful/rootless docker too
* if you don't have rootless podman/docker then `sudo make -C deps-docker` is fine too
* alternatively, you can entirely skip building the webdeps and instead extract the compiled webdeps from the latest github release with `./make-sfx.sh fast dl-wd`
* next, build `copyparty-sfx.py` by running `./make-sfx.sh gz fast`
* this is a dependency for most of the remaining steps, since they take the sfx as input
* removing `fast` makes it compress better
* removing `gz` too compresses even better, but startup gets slower
* if you want to build the `.pyz` standalone "binary", now run `./make-pyz.sh`
* if you want to build a pypi package, now run `./make-pypi-release.sh d`
* if you want to build a docker-image, you have two options:
* if you want to use podman to build all docker-images for all supported architectures, now run `(cd docker; ./make.sh hclean; ./make.sh hclean pull img)`
* if you want to use docker to build all docker-images for your native architecture, now run `sudo make -C docker`
* if you want to do something else, please take a look at `docker/make.sh` or `docker/Makefile` for inspiration
* if you want to build the windows exe, first grab some snacks and a beer, [you'll need it](https://github.com/9001/copyparty/tree/hovudstraum/scripts/pyinstaller)
the complete list of buildtime dependencies to do a build from scratch is as follows:
* on ubuntu-server, install podman or [docker](https://get.docker.com/), and then `sudo apt install make zip bzip2`
* because ubuntu is specifically what someone asked about :-p
## complete release
also builds the sfx so skip the sfx section above

View File

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

View File

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

View File

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

View File

@@ -106,3 +106,10 @@
/w/tank1
[/m8s]
/w/tank2
# some other things you can do:
# [/demo/${u%-su,%-fds}] # users which are NOT members of "su" or "fds"
# [/demo/${u%+su,%+fds}] # users which ARE members of BOTH "su" and "fds"
# [/demo/${g%-su}] # all groups except su
# [/demo/${g%-su,%-fds}] # all groups except su and fds

View File

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

View File

@@ -161,13 +161,14 @@ symbol legend,
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | | █ | █ |
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | | |
| upload segmenting | █ | | | | | | | █ | █ | █ | █ | | █ |
| upload segmenting | █ | | | | | | | █ | █ | █ | █ | | █ |
| upload acceleration | █ | | | | | | | | █ | | █ | | |
| upload verification | █ | | | █ | █ | | | | █ | | | | |
| upload deduplication | █ | | | | █ | | | | █ | | | | |
| upload a 999 TiB file | █ | | | | █ | █ | • | | █ | | █ | | |
| CTRL-V from device | █ | | | █ | | | | | | | | | |
| race the beam ("p2p") | █ | | | | | | | | | | | | |
| "tail -f" streaming | █ | | | | | | | | | | | | |
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
| upload rules | | | | | | | | | | | | | |
| ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ |
@@ -193,6 +194,8 @@ symbol legend,
* `race the beam` = files can be downloaded while they're still uploading; downloaders are slowed down such that the uploader is always ahead
* `tail -f` = when viewing or downloading a logfile, the connection can remain open to keep showing new lines as they are added in real time
* `upload routing` = depending on filetype / contents / uploader etc., the file can be redirected to another location or otherwise transformed; mitigates limitations such as [sharex#3992](https://github.com/ShareX/ShareX/issues/3992)
* copyparty example: [reloc-by-ext](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks#before-upload)
@@ -485,7 +488,7 @@ symbol legend,
* ⚠️ [isolated on-disk file hierarchy] in per-user folders
* not that bad, can probably be remedied with bindmounts or maybe symlinks
* ⚠️ uploads not resumable / accelerated / integrity-checked
* ⚠️ on cloudflare: max upload size 100 MiB
* 🔵 uploads are segmented; no filesize limit, even on cloudflare
* ⚠️ uploading small files is slow; `4` files per sec (copyparty does `670`/sec, 160x faster)
* ⚠️ no write-only / upload-only folders
* ⚠️ http/webdav only; no ftp, zeroconf

8
flake.lock generated
View File

@@ -17,16 +17,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1680334310,
"narHash": "sha256-ISWz16oGxBhF7wqAxefMPwFag6SlsA9up8muV79V9ck=",
"lastModified": 1748162331,
"narHash": "sha256-rqc2RKYTxP3tbjA+PB3VMRQNnjesrT0pEofXQTrMsS8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "884e3b68be02ff9d61a042bc9bd9dd2a358f95da",
"rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11",
"ref": "nixos-25.05",
"type": "indirect"
}
},

View File

@@ -1,28 +1,66 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-22.11";
nixpkgs.url = "nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
outputs =
{
self,
nixpkgs,
flake-utils,
}:
{
nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
overlays.default = self: super: {
copyparty =
self.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
ffmpeg = self.ffmpeg-full;
};
overlays.default = final: prev: rec {
copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
ffmpeg = final.ffmpeg-full;
};
partyfuse = prev.callPackage ./contrib/package/nix/partyfuse {
inherit copyparty;
};
u2c = prev.callPackage ./contrib/package/nix/u2c {
inherit copyparty;
};
};
} // flake-utils.lib.eachDefaultSystem (system:
}
// flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
config = {
allowAliases = false;
};
overlays = [ self.overlays.default ];
};
in {
in
{
# check that copyparty builds with all optionals turned on
checks.copyparty-full = self.packages.${system}.copyparty.override {
withHashedPasswords = true;
withCertgen = true;
withThumbnails = true;
withFastThumbnails = true;
withMediaProcessing = true;
withBasicAudioMetadata = true;
withZeroMQ = true;
withFTPS = true;
withSMB = true;
};
packages = {
inherit (pkgs) copyparty;
inherit (pkgs)
copyparty
partyfuse
u2c
;
default = self.packages.${system}.copyparty;
};
});
formatter = pkgs.nixfmt-tree;
}
);
}

View File

@@ -3,7 +3,7 @@ WORKDIR /z
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
ver_hashwasm=4.12.0 \
ver_marked=4.3.0 \
ver_dompf=3.2.5 \
ver_dompf=3.2.6 \
ver_mde=2.18.0 \
ver_codemirror=5.65.18 \
ver_fontawesome=5.13.0 \

View File

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

View File

@@ -12,16 +12,17 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
py3-pip py3-cffi \
ffmpeg \
py3-magic \
vips-jxl vips-heif vips-poppler vips-magick \
py3-numpy fftw libsndfile \
vamp-sdk vamp-sdk-libs \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev ffmpeg-dev fftw-dev libsndfile-dev \
py3-wheel py3-numpy-dev \
py3-wheel py3-numpy-dev libffi-dev \
vamp-sdk-dev \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \

View File

@@ -1,4 +1,7 @@
FROM debian:12-slim
FROM DO_NOT_USE_THIS_DOCKER_IMAGE
# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)
#FROM debian:12-slim
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \

View File

@@ -1,4 +1,7 @@
FROM fedora:39
FROM DO_NOT_USE_THIS_DOCKER_IMAGE
# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)
#FROM fedora:39
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \

View File

@@ -1,4 +1,7 @@
FROM fedora:38
FROM DO_NOT_USE_THIS_DOCKER_IMAGE
# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)
#FROM fedora:38
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \

View File

@@ -1,4 +1,7 @@
FROM ubuntu:23.04
FROM DO_NOT_USE_THIS_DOCKER_IMAGE
# this image is an unmaintained experiment to see whether alpine was the correct choice (it was)
#FROM ubuntu:23.04
WORKDIR /z
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \

View File

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

View File

@@ -9,13 +9,14 @@ ENV XDG_CONFIG_HOME=/cfg
RUN apk add -U !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
py3-pip py3-cffi \
ffmpeg \
py3-magic \
vips-jxl vips-heif vips-poppler vips-magick \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev py3-wheel \
python3-dev py3-wheel libffi-dev \
&& rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \
&& python3 -m pip install pyvips \
&& apk del py3-pip .bd

View File

@@ -28,6 +28,14 @@ all:
docker image ls
min:
rm -rf i
mkdir i
tar -cC../.. dist/copyparty-sfx.py bin/mtag | tar -xvCi
podman build --squash --pull=always -t copyparty/min:latest -f Dockerfile.min .
echo 'scale=1;'`podman save copyparty/min:latest | pigz -c | wc -c`/1024/1024 | bc
push:
docker push copyparty/min
docker push copyparty/im

View File

@@ -63,12 +63,13 @@ python3 -m copyparty \
--ign-ebind -p$((1024+RANDOM)),$((1024+RANDOM)),$((1024+RANDOM)) \
-v .::r --no-crt -qi127.1 --wr-h-eps $t & pid=$!
for n in $(seq 1 200); do sleep 0.2
for n in $(seq 1 900); do sleep 0.2
v=$(awk '/^127/{print;n=1;exit}END{exit n-1}' $t) && break
done
[ -z "$v" ] && echo SNAAAAAKE && exit 1
rm $t
for n in $(seq 1 200); do sleep 0.2
for n in $(seq 1 900); do sleep 0.2
wget -O- http://${v/ /:}/?tar=gz:1 >tf && break
done
tar -xzO top/innvikler.sh <tf | cmp innvikler.sh
@@ -79,7 +80,7 @@ kill $pid; wait $pid
########################################################################
# output from -e2d
rm -rf .hist
rm -rf .hist /cfg/copyparty
# goodbye
exec rm innvikler.sh

View File

@@ -7,7 +7,7 @@ import subprocess as sp
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
_ = r""""
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
"""
# click [edit] => [select all]

View File

@@ -23,7 +23,7 @@ exit 0
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c

View File

@@ -537,6 +537,7 @@ find | grep -E '\.(js|html)$' | while IFS= read -r f; do
done
gzres() {
local pk=
[ $zopf ] && command -v zopfli && pk="zopfli --i$zopf"
[ $zopf ] && command -v pigz && pk="pigz -11 -I $zopf"
[ -z "$pk" ] && pk='gzip'
@@ -628,7 +629,6 @@ suf=
[ $use_gz ] && {
sed -r 's/"r:bz2"/"r:gz"/' <$py >$py.t
py=$py.t
suf=-gz
}
"$pybin" $py --sfx-make tar.bz2 $ver $ts

View File

@@ -16,7 +16,7 @@ uname -s | grep WOW64 && m=64 || m=32
uname -s | grep NT-10 && w10=1 || w7=1
[ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh
[ $w7 ] && pyv=37 || pyv=312
[ $w7 ] && pyv=37 || pyv=313
esuf=
[ $w7 ] && [ $m = 32 ] && esuf=32
[ $w7 ] && [ $m = 64 ] && esuf=-winpe64
@@ -89,14 +89,18 @@ excl=(
urllib.request
urllib.response
urllib.robotparser
zipfile
)
[ $w10 ] && excl+=(
_pyrepl
distutils
setuptools
PIL._avif
PIL.ImageQt
PIL.ImageShow
PIL.ImageTk
PIL.ImageWin
PIL.PdfParser
zipimport
) || excl+=(
inspect
PIL
@@ -104,6 +108,7 @@ excl=(
PIL.Image
PIL.ImageDraw
PIL.ImageOps
zipfile
)
excl=( "${excl[@]/#/--exclude-module }" )

View File

@@ -3,7 +3,7 @@ f117016b1e6a7d7e745db30d3e67f1acf7957c443a0dd301b6c5e10b8368f2aa4db6be9782d2d3f8
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl
b297ff66ec50cf5a1abcf07d6ac949644c5150ba094ffac974c5d27c81574c3e97ed814a47547f4b03a4c83ea0fb8f026433fca06a3f08e32742dc5c024f3d07 pywin32_ctypes-0.2.3-py3-none-any.whl
085d39ef4426aa5f097fbc484595becc16e61ca23fc7da4d2a8bba540a3b82e789e390b176c7151bdc67d01735cce22b1562cdb2e31273225a2d3e275851a4ad setuptools-70.3.0-py3-none-any.whl
360a141928f4a7ec18a994602cbb28bbf8b5cc7c077a06ac76b54b12fa769ed95ca0333a5cf728923a8e0baeb5cc4d5e73e5b3de2666beb05eb477d8ae719093 upx-4.2.4-win32.zip
644931f8e1764e168c257c11c77b3d2ac5408397d97b0eef98168a058efe793d3ab6900dc2e9c54923a2bd906dd66bfbff8db6ff43418513e530a1bd501c6ccd upx-5.0.1-win32.zip
# win7
3253e86471e6f9fa85bfdb7684cd2f964ed6e35c6a4db87f81cca157c049bef43e66dfcae1e037b2fb904567b1e028aaeefe8983ba3255105df787406d2aa71e en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso
ab0db0283f61a5bbe44797d74546786bf41685175764a448d2e3bd629f292f1e7d829757b26be346b5044d78c9c1891736d93237cee4b1b6f5996a902c86d15f en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso
@@ -24,10 +24,11 @@ ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de
0a2cd4cadf0395f0374974cd2bc2407e5cc65c111275acdffb6ecc5a2026eee9e1bb3da528b35c7f0ff4b64563a74857d5c2149051e281cc09ebd0d1968be9aa en-us_windows_10_enterprise_ltsc_2021_x64_dvd_d289cf96.iso
16cc0c58b5df6c7040893089f3eb29c074aed61d76dae6cd628d8a89a05f6223ac5d7f3f709a12417c147594a87a94cc808d1e04a6f1e407cc41f7c9f47790d1 virtio-win-0.1.248.iso
9a7f40edc6f9209a2acd23793f3cbd6213c94f36064048cb8bf6eb04f1bdb2c2fe991cb09f77fe8b13e5cd85c618ef23573e79813b2fef899ab2f290cd129779 jinja2-3.1.6-py3-none-any.whl
6df21f0da408a89f6504417c7cdf9aaafe4ed88cfa13e9b8fa8414f604c0401f885a04bbad0484dc51a29284af5d1548e33c6cc6bfb9896d9992c1b1074f332d MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl
00731cfdd9d5c12efef04a7161c90c1e5ed1dc4677aa88a1d4054aff836f3430df4da5262ed4289c21637358a9e10e5df16f76743cbf5a29bb3a44b146c19cf3 MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
0203ec2551c4836696cfab0b2c9fff603352f03fa36e7476e2e1ca7ec57a3a0c24bd791fcd92f342bf817f0887854d9f072e0271c643de4b313d8c9569ba8813 packaging-24.1-py3-none-any.whl
c9051daaf34ec934962c743a5ac2dbe55a9b0cababb693a8cde0001d24d4a50b67bd534d714d935def6ca7b898ec0a352e58bd9ccdce01c54eaf2281b18e478d pillow-11.2.1-cp312-cp312-win_amd64.whl
f0463895e9aee97f31a2003323de235fed1b26289766dc0837261e3f4a594a31162b69e9adbb0e9a31e2e2d4b5f25c762ed1669553df7dc89a8ba4f85d297873 pyinstaller-6.11.1-py3-none-win_amd64.whl
d550a0a14428386945533de2220c4c2e37c0c890fc51a600f626c6ca90a32d39572c121ec04c157ba3a8d6601cb021f8433d871b5c562a3d342c804fffec90c1 pyinstaller_hooks_contrib-2024.11-py3-none-any.whl
4f9a4d9f65c93e2d851e2674057343a9599f30f5dc582ffca485522237d4fcf43653b3d393ed5eb11e518c4ba93714a07134bbb13a97d421cce211e1da34682e python-3.12.10-amd64.exe
a726fb46cce24f781fc8b55a3e6dea0a884ebc3b2b400ea74aa02333699f4955a5dc1e2ec5927ac72f35a624401f3f3b442882ba1cc4cadaf9c88558b5b8bdae packaging-25.0-py3-none-any.whl
3e39ea6e16b502d99a2e6544579095d0f7c6097761cd85135d5e929b9dec1b32e80669a846f94ee8c2cca9be2f5fe728625d09453988864c04e16bb8445c3f91 pillow-11.3.0-cp313-cp313-win_amd64.whl
59fbbcae044f4ee73d203ac74b553b27bfad3e6b2f3fb290fd3f8774753c6b545176b6b3399c240b092d131d152290ce732750accd962dc1e48e930be85f5e53 pyinstaller-6.14.1-py3-none-win_amd64.whl
fc6f3e144c5f5b662412de07cb8bf0c2eb3b3be21d19ec448aef3c4244d779b9ab8027fd67a4871e6e13823b248ea0f5a7a9241a53aef30f3b51a6d3cb5bdb3f pyinstaller_hooks_contrib-2025.5-py3-none-any.whl
2c7a52e223b8186c21009d3fa5ed6a856d8eb4ef3b98f5d24c378c6a1afbfa1378bd7a51d6addc500e263d7989efb544c862bf920055e740f137c702dfd9d18b python-3.13.5-amd64.exe
2a0420f7faaa33d2132b82895a8282688030e939db0225ad8abb95a47bdb87b45318f10985fc3cee271a9121441c1526caa363d7f2e4a4b18b1a674068766e87 setuptools-80.9.0-py3-none-any.whl

View File

@@ -29,19 +29,19 @@ uname -s | grep NT-10 && w10=1 || {
fns=(
altgraph-0.17.4-py2.py3-none-any.whl
pefile-2023.2.7-py3-none-any.whl
pywin32_ctypes-0.2.2-py3-none-any.whl
setuptools-70.3.0-py3-none-any.whl
upx-4.2.4-win32.zip
pywin32_ctypes-0.2.3-py3-none-any.whl
upx-5.0.1-win32.zip
)
[ $w10 ] && fns+=(
jinja2-3.1.6-py3-none-any.whl
MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl
MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl
mutagen-1.47.0-py3-none-any.whl
packaging-24.1-py3-none-any.whl
pillow-11.2.1-cp312-cp312-win_amd64.whl
pyinstaller-6.10.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2024.8-py3-none-any.whl
python-3.12.10-amd64.exe
packaging-25.0-py3-none-any.whl
pillow-11.3.0-cp313-cp313-win_amd64.whl
pyinstaller-6.14.1-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2025.5-py3-none-any.whl
python-3.13.5-amd64.exe
setuptools-80.9.0-py3-none-any.whl
)
[ $w7 ] && fns+=(
future-1.0.0-py3-none-any.whl
@@ -49,6 +49,7 @@ fns=(
packaging-24.0-py3-none-any.whl
pip-24.0-py3-none-any.whl
pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl
setuptools-70.3.0-py3-none-any.whl
typing_extensions-4.7.1-py3-none-any.whl
zipp-3.15.0-py3-none-any.whl
)
@@ -80,7 +81,7 @@ close and reopen git-bash so python is in PATH
===[ copy-paste into git-bash ]================================
uname -s | grep NT-10 && w10=1 || w7=1
[ $w7 ] && pyv=37 || pyv=312
[ $w7 ] && pyv=37 || pyv=313
appd=$(cygpath.exe "$APPDATA")
cd ~/Downloads &&
yes | unzip upx-*-win32.zip &&

View File

@@ -34,6 +34,7 @@ shift
./make-sfx.sh "$@"
f=../dist/copyparty-sfx
[ -e $f.py ] && s= || s=-gz
# TODO: the -gz suffix is gone, can drop all the $s stuff probably
$f$s.py --version >/dev/null

View File

@@ -94,6 +94,7 @@ copyparty/web/deps/prismd.css,
copyparty/web/deps/scp.woff2,
copyparty/web/deps/sha512.ac.js,
copyparty/web/deps/sha512.hw.js,
copyparty/web/idp.html,
copyparty/web/iiam.gif,
copyparty/web/md.css,
copyparty/web/md.html,

View File

@@ -121,7 +121,7 @@ var tl_browser = {
"file-manager",
["G", "toggle list / grid view"],
["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"],
[" A/D", "thumbnail size"],
["ctrl-K", "delete selected"],
["ctrl-X", "cut selection to clipboard"],
["ctrl-C", "copy selection to clipboard"],
@@ -131,9 +131,9 @@ var tl_browser = {
"file-list-sel",
["space", "toggle file selection"],
["🡑/🡓", "move selection cursor"],
["ctrl 🡑/🡓", "move cursor and viewport"],
["🡅 🡑/🡓", "select prev/next file"],
["↑/↓", "move selection cursor"],
["ctrl ↑/↓", "move cursor and viewport"],
["⇧ ↑/↓", "select prev/next file"],
["ctrl-A", "select all files / folders"],
], [
"navigation",
@@ -156,7 +156,7 @@ var tl_browser = {
["Home/End", "first/last pic"],
["F", "fullscreen"],
["R", "rotate clockwise"],
["🡅 R", "rotate ccw"],
[" R", "rotate ccw"],
["S", "select pic"],
["Y", "download pic"],
], [
@@ -226,10 +226,13 @@ var tl_browser = {
"wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V",
"wt_selall": "select all files$NHotkey: ctrl-A (when file focused)",
"wt_selinv": "invert selection",
"wt_zip1": "download this folder as archive",
"wt_selzip": "download selection as archive",
"wt_seldl": "download selection as separate files$NHotkey: Y",
"wt_npirc": "copy irc-formatted track info",
"wt_nptxt": "copy plaintext track info",
"wt_m3ua": "add to m3u playlist (click <code>📻copy</code> later)",
"wt_m3uc": "copy m3u playlist to clipboard",
"wt_grid": "toggle grid / list view$NHotkey: G",
"wt_prev": "previous track$NHotkey: J",
"wt_play": "play / pause$NHotkey: P",
@@ -309,6 +312,7 @@ var tl_browser = {
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
"ct_dots": 'show hidden files (if server permits)">dotfiles',
"ct_qdel": 'when deleting files, only ask for confirmation once">qdel',
"ct_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme',
@@ -332,6 +336,8 @@ var tl_browser = {
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$Nmakes https 30% faster, http 4.5x faster\">mt",
"cut_wasm": "use wasm instead of the browser's built-in hasher; improves speed on chrome-based browsers but increases CPU load, and many older versions of chrome have bugs which makes the browser consume all RAM and crash if this is enabled\">wasm",
"cft_text": "favicon text (blank and refresh to disable)",
"cft_fg": "foreground color",
"cft_bg": "background color",
@@ -358,6 +364,7 @@ var tl_browser = {
"ml_drc": "dynamic range compressor",
"mt_loop": "loop/repeat one song\">🔁",
"mt_one": "stop after one song\">1⃣",
"mt_shuf": "shuffle the songs in each folder\">🔀",
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
"mt_preload": "start loading the next song near the end for gapless playback\">preload",
@@ -366,6 +373,7 @@ var tl_browser = {
"mt_fau": "on phones, prevent music from stopping if the next song doesn't preload fast enough (can make tags display glitchy)\">☕️",
"mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s",
"mt_npclip": "show buttons for clipboarding the currently playing song\">/np",
"mt_m3u_c": "show buttons for clipboarding the$Nselected songs as m3u8 playlist entries\">📻",
"mt_octl": "os integration (media hotkeys / osd)\">os-ctl",
"mt_oseek": "allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\">seek",
"mt_oscv": "show album cover in osd\">art",
@@ -374,6 +382,7 @@ var tl_browser = {
"mt_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
"mt_mloop": "loop the open folder\">🔁 loop",
"mt_mnext": "load the next folder and continue\">📂 next",
"mt_mstop": "stop playback\">⏸ stop",
"mt_cflac": "convert flac / wav to opus\">flac",
"mt_caac": "convert aac / m4a to opus\">aac",
"mt_coth": "convert all others (not mp3) to opus\">oth",
@@ -391,6 +400,7 @@ var tl_browser = {
"mb_play": "play",
"mm_hashplay": "play this audio file?",
"mm_m3u": "press <code>Enter/OK</code> to Play\npress <code>ESC/Cancel</code> to Edit",
"mp_breq": "need firefox 82+ or chrome 73+ or iOS 15+",
"mm_bload": "now loading...",
"mm_bconv": "converting to {0}, please wait...",
@@ -416,6 +426,7 @@ var tl_browser = {
"f_empty": 'this folder is empty',
"f_chide": 'this will hide the column «{0}»\n\nyou can unhide columns in the settings tab',
"f_bigtxt": "this file is {0} MiB large -- really view as text?",
"f_bigtxt2": "view just the end of the file instead? this will also enable following/tailing, showing newly added lines of text in real time",
"fbd_more": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_more">show {2}</a> or <a href="#" id="bd_all">show all</a></div>',
"fbd_all": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_all">show all</a></div>',
"f_anota": "only {0} of the {1} items were selected;\nto select the full folder, first scroll to the bottom",
@@ -520,6 +531,15 @@ var tl_browser = {
"tvt_next": "show next document$NHotkey: K\">⬇ next",
"tvt_sel": "select file &nbsp; ( for cut / copy / delete / ... )$NHotkey: S\">sel",
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
"tvt_tail": "monitor file for changes; show new lines in real time\">📡 follow",
"tvt_wrap": "word-wrap\">↵",
"tvt_atail": "lock scroll to bottom of page\">⚓",
"tvt_ctail": "decode terminal colors (ansi escape codes)\">🌈",
"tvt_ntail": "scrollback limit (how many bytes of text to keep loaded)",
"m3u_add1": "song added to m3u playlist",
"m3u_addn": "{0} songs added to m3u playlist",
"m3u_clip": "m3u playlist now copied to clipboard\n\nyou should create a new textfile named something.m3u and paste the playlist in that document; this will make it playable",
"gt_vau": "don't show videos, just play the audio\">🎧",
"gt_msel": "enable file selection; ctrl-click a file to override$N$N&lt;em&gt;when active: doubleclick a file / folder to open it&lt;/em&gt;$N$NHotkey: S\">multiselect",
@@ -615,6 +635,7 @@ var tl_browser = {
"u_https3": "for better performance",
"u_ancient": 'your browser is impressively ancient -- maybe you should <a href="#" onclick="goto(\'bup\')">use bup instead</a>',
"u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+",
"tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+",
"u_nodrop": 'your browser is too old for drag-and-drop uploading',
"u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead",
"u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button",

46
tests/res/idp/7.conf Normal file
View File

@@ -0,0 +1,46 @@
# -*- mode: yaml -*-
# vim: ft=yaml:
[global]
idp-h-usr: x-idp-user
idp-h-grp: x-idp-group
[/u/${u}]
/u/${u}
accs:
r: *
[/uya/${u%+ga}]
/uya/${u}
accs:
r: *
[/uyab/${u%+ga,%+gb}]
/uyab/${u}
accs:
r: *
[/una/${u%-ga}]
/una/${u}
accs:
r: *
[/unab/${u%-ga,%-gb}]
/unab/${u}
accs:
r: *
[/gya/${g%+ga}]
/gya/${g}
accs:
r: *
[/gna/${g%-ga}]
/gna/${g}
accs:
r: *
[/gnab/${g%-ga,%-gb}]
/gnab/${g}
accs:
r: *

47
tests/res/idp/8.conf Normal file
View File

@@ -0,0 +1,47 @@
# -*- mode: yaml -*-
# vim: ft=yaml:
[groups]
ga: iua, iuab, iuabc
gb: iuab, iuabc, iub, iubc
gc: iuabc, iubc, iuc
[/u/${u}]
/u/${u}
accs:
r: *
[/uya/${u%+ga}]
/uya/${u}
accs:
r: *
[/uyab/${u%+ga,%+gb}]
/uyab/${u}
accs:
r: *
[/una/${u%-ga}]
/una/${u}
accs:
r: *
[/unab/${u%-ga,%-gb}]
/unab/${u}
accs:
r: *
[/gya/${g%+ga}]
/gya/${g}
accs:
r: *
[/gna/${g%-ga}]
/gna/${g}
accs:
r: *
[/gnab/${g%-ga,%-gb}]
/gnab/${g}
accs:
r: *

View File

@@ -234,3 +234,74 @@ class TestVFS(unittest.TestCase):
au.idp_checkin(None, "iud", "su")
self.assertAxsAt(au, "team/su/iuc", [["iuc", "iud"]])
self.assertAxsAt(au, "team/su/iud", [["iuc", "iud"]])
def test_7(self):
"""
conditional idp-vols
"""
_, cfgdir, xcfg = self.prep()
au = AuthSrv(Cfg(c=[cfgdir + "/7.conf"], **xcfg), self.log)
au.idp_checkin(None, "iua", "ga")
au.idp_checkin(None, "iuab", "ga,gb")
au.idp_checkin(None, "iuabc", "ga,gb,gc")
au.idp_checkin(None, "iub", "gb")
au.idp_checkin(None, "iubc", "gb,gc")
au.idp_checkin(None, "iuc", "gc")
zs = """
u/iua
u/iuab
u/iuabc
u/iub
u/iubc
u/iuc
uya/iua
uya/iuab
uya/iuabc
uyab/iuab
uyab/iuabc
una/iub
una/iubc
una/iuc
unab/iuc
gya/ga
gna/gb
gna/gc
gnab/gc
"""
zl1 = sorted(zs.strip().split("\n"))[:]
zl2 = sorted(list(au.vfs.all_vols))[:]
# print(" ".join(zl1))
# print(" ".join(zl2))
self.assertListEqual(zl1, zl2)
def test_8(self):
"""
conditional non-idp vols
"""
_, cfgdir, xcfg = self.prep()
xcfg = {"vc": True}
au = AuthSrv(Cfg(c=[cfgdir + "/8.conf"], **xcfg), self.log)
zs = """
u/iua
u/iuab
u/iuabc
u/iub
u/iubc
u/iuc
uya/iua
uya/iuab
uya/iuabc
uyab/iuab
uyab/iuabc
una/iub
una/iubc
una/iuc
unab/iuc
gya/ga
gna/gb
gna/gc
gnab/gc
"""
zl1 = sorted(zs.strip().split("\n"))[:]
zl2 = sorted(list(au.vfs.all_vols))[:]
self.assertListEqual(zl1, zl2)

229
tests/test_shr.py Normal file
View File

@@ -0,0 +1,229 @@
#!/usr/bin/env python3
# coding: utf-8
from __future__ import print_function, unicode_literals
import json
import os
import shutil
import sqlite3
import tempfile
import unittest
from copyparty.__init__ import ANYWIN
from copyparty.authsrv import AuthSrv
from copyparty.httpcli import HttpCli
from copyparty.util import absreal
from tests import util as tu
from tests.util import Cfg
class TestShr(unittest.TestCase):
def log(self, src, msg, c=0):
m = "%s" % (msg,)
if (
"warning: filesystem-path does not exist:" in m
or "you are sharing a system directory:" in m
or "symlink-based deduplication is enabled" in m
or m.startswith("hint: argument")
):
return
print(("[%s] %s" % (src, msg)).encode("ascii", "replace").decode("ascii"))
def assertLD(self, url, auth, els, edl):
ls = self.ls(url, auth)
self.assertEqual(ls[0], len(els) == 2)
if not ls[0]:
return
a = [list(sorted(els[0])), list(sorted(els[1]))]
b = [list(sorted(ls[1])), list(sorted(ls[2]))]
self.assertEqual(a, b)
if edl is None:
edl = els[1]
can_dl = []
for fn in b[1]:
if fn == "a.db":
continue
furl = url + "/" + fn
if auth:
furl += "?pw=p1"
h, zb = self.curl(furl, True)
if h.startswith("HTTP/1.1 200 "):
can_dl.append(fn)
self.assertEqual(edl, can_dl)
def setUp(self):
self.td = tu.get_ramdisk()
td = os.path.join(self.td, "vfs")
os.mkdir(td)
os.chdir(td)
os.mkdir("d1")
os.mkdir("d2")
os.mkdir("d2/d3")
for zs in ("d1/f1", "d2/f2", "d2/d3/f3"):
with open(zs, "wb") as f:
f.write(zs.encode("utf-8"))
for dst in ("d1", "d2", "d2/d3"):
src, fn = zs.rsplit("/", 1)
os.symlink(absreal(zs), dst + "/l" + fn[-1:])
db = sqlite3.connect("a.db")
with db:
zs = r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)"
db.execute(zs)
db.close()
def tearDown(self):
os.chdir(tempfile.gettempdir())
shutil.rmtree(self.td)
def cinit(self):
self.asrv = AuthSrv(self.args, self.log)
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
def test1(self):
self.args = Cfg(
a=["u1:p1"],
v=["::A,u1", "d1:v1:A,u1", "d2/d3:d2/d3:A,u1"],
shr="/shr/",
shr1="shr/",
shr_db="a.db",
shr_v=False,
)
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
jt = {
"k": "r",
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d2",
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("shr/d2/d3", False, [], None)
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD("shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("shr/r/d2", False, [], None) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], None)
self.conn.shutdown()
def test2(self):
self.args = Cfg(
a=["u1:p1"],
v=["::A,u1", "d1:v1:A,u1", "d2/d3:d2/d3:A,u1"],
shr="/shr/",
shr1="shr/",
shr_db="a.db",
shr_v=False,
xvol=True,
)
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
jt = {
"k": "r",
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d2",
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], ["f2", "l2"])
self.assertLD("shr/d2/d3", False, [], [])
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD(
"shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], ["f1", "l1", "l2"]
)
self.assertLD("shr/r/d2", False, [], []) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], [])
self.conn.shutdown()
def ls(self, url: str, auth: bool):
zs = url + "?ls" + ("&pw=p1" if auth else "")
h, b = self.curl(zs)
if not h.startswith("HTTP/1.1 200 "):
return (False, [], [])
jo = json.loads(b)
return (
True,
[x["href"].rstrip("/") for x in jo.get("dirs") or {}],
[x["href"] for x in jo.get("files") or {}],
)
def curl(self, url: str, binary=False):
h = "GET /%s HTTP/1.1\r\nConnection: close\r\n\r\n"
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
if binary:
h, b = self.conn.s._reply.split(b"\r\n\r\n", 1)
return [h.decode("utf-8"), b]
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def post_json(self, url: str, data):
buf = json.dumps(data).encode("utf-8")
msg = [
"POST /%s HTTP/1.1" % (url,),
"Connection: close",
"Content-Type: application/json",
"Content-Length: %d" % (len(buf),),
"\r\n",
]
buf = "\r\n".join(msg).encode("utf-8") + buf
print("PUT -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)

View File

@@ -82,6 +82,19 @@ def get_ramdisk():
return subdir(vol)
if os.path.exists("/Volumes"):
sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
while True:
try:
sck.bind(("127.0.0.1", 2775))
break
except:
print("waiting for 2775")
time.sleep(0.5)
v = "/Volumes/cptd"
if os.path.exists(v):
return subdir(v)
# hdiutil eject /Volumes/cptd/
devname, _ = chkcmd("hdiutil attach -nomount ram://131072".split())
devname = devname.strip()
@@ -97,6 +110,7 @@ def get_ramdisk():
except:
pass
sck.close()
return subdir("/Volumes/cptd")
except Exception as ex:
print(repr(ex))
@@ -129,28 +143,31 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "chpw daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ex = "chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip"
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
ka.update(**{k: None for k in ex.split()})
ex = "hash_mt hsortn safe_dedup srch_time u2abort u2j u2sz"
ex = "gid uid"
ka.update(**{k: -1 for k in ex.split()})
ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody th_convt ups_who zip_who"
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
ka.update(**{k: 9 for k in ex.split()})
ex = "db_act forget_ip k304 loris no304 re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
ex = "db_act forget_ip idp_store k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
ka.update(**{k: "" for k in ex.split()})
ex = "ban_403 ban_404 ban_422 ban_pw ban_url spinner"
ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner"
ka.update(**{k: "no" for k in ex.split()})
ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
@@ -166,6 +183,8 @@ class Cfg(Namespace):
v=v or [],
c=c,
E=E,
bup_ck="sha512",
chmod_d="755",
dbd="wal",
dk_salt="b" * 16,
fk_salt="a" * 16,
@@ -178,6 +197,8 @@ class Cfg(Namespace):
mte={"a": True},
mth={},
mtp=[],
put_ck="sha512",
put_name="put-{now.6f}-{cip}.bin",
mv_retry="0/0",
rm_retry="0/0",
s_rd_sz=256 * 1024,
@@ -243,6 +264,9 @@ class VHub(object):
self.is_dut = True
self.up2k = Up2k(self)
def reload(self, a, b):
pass
class VBrokerThr(BrokerThr):
def __init__(self, hub):