Compare commits

...

58 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
44 changed files with 1693 additions and 448 deletions

View File

@@ -12,7 +12,7 @@ turn almost any device into a file server with resumable uploads/downloads using
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) 📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) 🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) // 👉 **[feature-showcase](https://a.ocv.me/pub/demo/showcase-hq.webm)** ([youtube](https://www.youtube.com/watch?v=15_-hgsX2V0))
made in Norway 🇳🇴 made in Norway 🇳🇴
@@ -80,6 +80,7 @@ made in Norway 🇳🇴
* [periodic rescan](#periodic-rescan) - filesystem monitoring * [periodic rescan](#periodic-rescan) - filesystem monitoring
* [upload rules](#upload-rules) - set upload rules using volflags * [upload rules](#upload-rules) - set upload rules using volflags
* [compress uploads](#compress-uploads) - files can be autocompressed on upload * [compress uploads](#compress-uploads) - files can be autocompressed on upload
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
* [other flags](#other-flags) * [other flags](#other-flags)
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
@@ -1439,12 +1440,17 @@ if you enable deduplication with `--dedup` then it'll create a symlink instead o
**warning:** when enabling dedup, you should also: **warning:** when enabling dedup, you should also:
* enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended * enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended
* ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below * ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below
* ...and/or `--reflink` to use CoW/reflink-based dedup (much safer than hardlink, but OS/FS-dependent)
it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to) it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to)
by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`; you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`, and you can choose to use reflinks with `--reflink` or volflag `reflink`
advantages of using reflinks (CoW, copy-on-write):
* entirely safe (when your filesystem supports it correctly); either file can be edited or deleted without affecting other copies
* only linux 5.3 or newer, only python 3.14 or newer, only some filesystems (btrfs probably ok, maybe xfs too, but zfs had bugs)
advantages of using hardlinks: advantages of using hardlinks:
* hardlinks are more compatible with other software; they behave entirely like regular files * hardlinks are more compatible with other software; they behave entirely like regular files
@@ -1605,7 +1611,7 @@ config file example:
w: * # anyone can upload here w: * # anyone can upload here
rw: ed # only user "ed" can read-write rw: ed # only user "ed" can read-write
flags: 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 sz: 1k-3m # accept upload only if filesize in this range
df: 4g # free disk space cannot go lower than this df: 4g # free disk space cannot go lower than this
vmaxb: 1g # volume can never exceed 1 GiB vmaxb: 1g # volume can never exceed 1 GiB
@@ -1644,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` allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4`
## chmod and chown
per-volume filesystem-permissions and ownership
by default:
* all folders are chmod 755
* files are usually chmod 644 (umask-defined)
* user/group is whatever copyparty is running as
this can be configured per-volume:
* volflag `chmod_f` sets file permissions; default=`644` (usually)
* volflag `chmod_d` sets directory permissions; default=`755`
* volflag `uid` sets the owner user-id
* volflag `gid` sets the owner group-id
notes:
* `gid` can only be set to one of the groups which the copyparty process is a member of
* `uid` can only be set if copyparty is running as root (i appreciate your faith)
## other flags ## other flags
* `:c,magic` enables filetype detection for nameless uploads, same as `--magic` * `:c,magic` enables filetype detection for nameless uploads, same as `--magic`
@@ -1662,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 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: note:
* putting the hist-folders on an SSD is strongly recommended for performance * putting the hist-folders on an SSD is strongly recommended for performance
* markdown edits are always stored in a local `.hist` subdirectory * markdown edits are always stored in a local `.hist` subdirectory
@@ -1679,6 +1707,8 @@ config file example:
flags: flags:
hist: - # restore the default (/mnt/nas/pics/.hist/) hist: - # restore the default (/mnt/nas/pics/.hist/)
hist: /mnt/nas/cache/pics/ # can be absolute path 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"
``` ```
@@ -2018,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 * **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2 * depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/tmp/party.sock` (permission `770` means only members of group `www` can access it) for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/dev/shm/party.sock` (permission `770` means only members of group `www` can access it)
example webserver / reverse-proxy configs: example webserver / reverse-proxy configs:
@@ -2217,6 +2247,7 @@ force-enable features with known issues on your OS/env by setting any of the fo
| env-var | what it does | | env-var | what it does |
| ------------------------ | ------------ | | ------------------------ | ------------ |
| `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms | | `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
| `PRTY_FORCE_MAGIC` | use [magic](https://pypi.org/project/python-magic/) on Windows (you will segfault) |
# packages # packages
@@ -2239,7 +2270,7 @@ NOTE: there used to be an aur package; this evaporated when copyparty was adopte
## fedora package ## fedora package
does not exist yet; using the [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) builds is **NOT recommended** because updates can be delayed by [several months](https://github.com/fedora-copr/copr/issues/3056) does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
## nix package ## nix package

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty pkgname=copyparty
pkgver="1.18.2" pkgver="1.18.7"
pkgrel=1 pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any") arch=("any")
@@ -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") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" ) backup=("etc/${pkgname}.d/init" )
sha256sums=("7bcd6fad7c1612a29320e9eb5e8f1bf3852c35df03a04ecb348536028ce31a37") sha256sums=("6738f623905276e8664bd8791f1497d0c61f8816c6608e76c4d9097a12849170")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,41 +1,67 @@
{ lib, stdenv, makeWrapper, fetchurl, util-linux, 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) # use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true, withHashedPasswords ? true,
# generate TLS certificates on startup (pointless when reverse-proxied) # generate TLS certificates on startup (pointless when reverse-proxied)
withCertgen ? false, withCertgen ? false,
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withThumbnails ? true, withThumbnails ? true,
# create thumbnails with PyVIPS; even faster, uses more memory # create thumbnails with PyVIPS; even faster, uses more memory
# -- can be combined with Pillow to support more filetypes # -- can be combined with Pillow to support more filetypes
withFastThumbnails ? false, withFastThumbnails ? false,
# enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus # enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus
# -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface # -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface
# -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both # -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both
withMediaProcessing ? true, withMediaProcessing ? true,
# if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster) # if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster)
withBasicAudioMetadata ? false, withBasicAudioMetadata ? false,
# send ZeroMQ messages from event-hooks # send ZeroMQ messages from event-hooks
withZeroMQ ? true, withZeroMQ ? true,
# enable FTPS support in the FTP server # enable FTPS support in the FTP server
withFTPS ? false, withFTPS ? false,
# samba/cifs server; dangerous and buggy, enable if you really need it # samba/cifs server; dangerous and buggy, enable if you really need it
withSMB ? false, 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 let
pinData = lib.importJSON ./pin.json; pinData = lib.importJSON ./pin.json;
pyEnv = python.withPackages (ps: pyEnv = python.withPackages (
with ps; [ ps:
with ps;
[
jinja2 jinja2
] ]
++ lib.optional withSMB impacket ++ lib.optional withSMB impacket
@@ -47,22 +73,36 @@ let
++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi ++ lib.optional withHashedPasswords argon2-cffi
++ lib.optional withZeroMQ pyzmq ++ lib.optional withZeroMQ pyzmq
); ++ (extraPythonPackages ps)
in stdenv.mkDerivation { );
runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg);
in
stdenv.mkDerivation {
pname = "copyparty"; pname = "copyparty";
version = pinData.version; inherit (pinData) version;
src = fetchurl { src = fetchurl {
url = pinData.url; inherit (pinData) url hash;
hash = pinData.hash;
}; };
buildInputs = [ makeWrapper ]; nativeBuildInputs = [ makeWrapper ];
dontUnpack = true; dontUnpack = true;
dontBuild = true;
installPhase = '' installPhase = ''
install -Dm755 $src $out/share/copyparty-sfx.py install -Dm755 $src $out/share/copyparty-sfx.py
makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \ makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \
--set PATH '${lib.makeBinPath ([ util-linux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \ --prefix PATH : ${lib.makeBinPath runtimeDeps} \
--add-flags "$out/share/copyparty-sfx.py" --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.18.2/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.18.7/copyparty-sfx.py",
"version": "1.18.2", "version": "1.18.7",
"hash": "sha256-l6rZlZG7y25ds23ZqBA6BjzYY8WLTMAmfsGoWeYt0mQ=" "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

@@ -53,13 +53,13 @@ from .util import (
PYFTPD_VER, PYFTPD_VER,
RAM_AVAIL, RAM_AVAIL,
RAM_TOTAL, RAM_TOTAL,
RE_ANSI,
SQLITE_VER, SQLITE_VER,
UNPLICATIONS, UNPLICATIONS,
URL_BUG, URL_BUG,
URL_PRJ, URL_PRJ,
Daemon, Daemon,
align_tab, align_tab,
ansi_re,
b64enc, b64enc,
dedent, dedent,
has_resource, has_resource,
@@ -167,7 +167,7 @@ def lprint(*a: Any, **ka: Any) -> None:
txt: str = " ".join(unicode(x) for x in a) + eol txt: str = " ".join(unicode(x) for x in a) + eol
printed.append(txt) printed.append(txt)
if not VT100: if not VT100:
txt = ansi_re.sub("", txt) txt = RE_ANSI.sub("", txt)
print(txt, end="", **ka) print(txt, end="", **ka)
@@ -547,14 +547,15 @@ def get_sects():
when running behind a reverse-proxy, it's recommended to when running behind a reverse-proxy, it's recommended to
use unix-sockets for improved performance and security; use unix-sockets for improved performance and security;
\033[32m-i unix:770:www:\033[33m/tmp/a.sock\033[0m listens on \033[33m/tmp/a.sock\033[0m with \033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on
permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m \033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m;
group. This is the best approach. Alternatively, only accessible to members of the \033[33mwww\033[0m group.
This is the best approach. Alternatively,
\033[32m-i unix:777:\033[33m/tmp/a.sock\033[0m sets perms \033[33m0777\033[0m so anyone can \033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone
access it; bad unless it's inside a restricted folder can access it; bad unless it's inside a restricted folder
\033[32m-i unix:\033[33m/tmp/a.sock\033[0m keeps umask-defined permissions \033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission
(usually \033[33m0600\033[0m) and the same user/group as copyparty (usually \033[33m0600\033[0m) and the same user/group as copyparty
\033[33m-p\033[0m (tcp ports) is ignored for unix sockets \033[33m-p\033[0m (tcp ports) is ignored for unix sockets
@@ -872,31 +873,31 @@ def get_sects():
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
the value is a three-digit octal number such as 755, 750, 644, etc. the value is a three-digit octal number such as \033[32m755\033[0m, \033[32m750\033[0m, \033[32m644\033[0m, etc.
first digit = "User"; permission for the unix-user first digit = "User"; permission for the unix-user
second digit = "Group"; permission for the unix-group second digit = "Group"; permission for the unix-group
third digit = "Other"; permission for all other users/groups third digit = "Other"; permission for all other users/groups
for files: for files:
0 = --- = no access \033[32m0\033[0m = \033[35m---\033[0m = no access
1 = --x = can execute the file as a program \033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
2 = -w- = can write \033[32m2\033[0m = \033[35m-w-\033[0m = can write
3 = -wx = can write and execute \033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
4 = r-- = can read \033[32m4\033[0m = \033[35mr--\033[0m = can read
5 = r-x = can read and execute \033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
6 = rw- = can read and write \033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
7 = rwx = can read, write, execute \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute
for directories/folders: for directories/folders:
0 = --- = no access \033[32m0\033[0m = \033[35m---\033[0m = no access
1 = --x = can read files in folder but not list contents \033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
2 = -w- = n/a \033[32m2\033[0m = \033[35m-w-\033[0m = n/a
3 = -wx = can create files but not list \033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
4 = r-- = can list, but not read/write \033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
5 = r-x = can list and read files \033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
6 = rw- = n/a \033[32m6\033[0m = \033[35mrw-\033[0m = n/a
7 = rwx = can read, write, list \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
""" """
), ),
], ],
@@ -1052,10 +1053,13 @@ def add_upload(ap):
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)") ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)") ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)") ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)") ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)") ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)") ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)") ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)") ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)") ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually") ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
@@ -1288,6 +1292,7 @@ def add_stats(ap):
def add_yolo(ap): def add_yolo(ap):
ap2 = ap.add_argument_group('yolo options') ap2 = ap.add_argument_group('yolo options')
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests") ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
@@ -1335,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("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month") ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)") ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
@@ -1550,7 +1556,8 @@ def add_ui(ap, retry):
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)") ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)") ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions") ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
ap2.add_argument("--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("--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("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)") ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])") ap2.add_argument("--mpmc", 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])")
@@ -1564,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("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents") ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-np\033[0m") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)") ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")

View File

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

View File

@@ -33,6 +33,7 @@ from .util import (
afsenc, afsenc,
get_df, get_df,
humansize, humansize,
json_hesc,
min_ex, min_ex,
odfusion, odfusion,
read_utf8, read_utf8,
@@ -70,6 +71,25 @@ if PY2:
LEELOO_DALLAS = "leeloo_dallas" LEELOO_DALLAS = "leeloo_dallas"
##
## you might be curious what Leeloo Dallas is doing here, so let me explain:
##
## certain daemonic tasks, namely:
## * deletion of expired files, running on a timer
## * deletion of sidecar files, initiated by plugins
## need to skip the usual permission-checks to do their thing,
## so we let Leeloo handle these
##
## and also, the smb-server has really shitty support for user-accounts
## so one popular way to avoid issues is by running copyparty without users;
## this makes all smb-clients identify as LD to gain unrestricted access
##
## Leeloo, being a fictional character from The Fifth Element,
## obviously does not exist and will never be able to access any copyparty
## instances from the outside (the username is rejected at every entrypoint)
##
## thanks for coming to my ted talk
SEE_LOG = "see log for details" SEE_LOG = "see log for details"
SEESLOG = " (see serverlog for details)" SEESLOG = " (see serverlog for details)"
@@ -121,6 +141,8 @@ class Lim(object):
self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry
self.chmod_d = 0o755 self.chmod_d = 0o755
self.uid = self.gid = -1
self.chown = False
self.nups: dict[str, list[float]] = {} # num tracker self.nups: dict[str, list[float]] = {} # num tracker
self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list
@@ -283,6 +305,8 @@ class Lim(object):
# no branches yet; make one # no branches yet; make one
sub = os.path.join(path, "0") sub = os.path.join(path, "0")
bos.mkdir(sub, self.chmod_d) bos.mkdir(sub, self.chmod_d)
if self.chown:
os.chown(sub, self.uid, self.gid)
else: else:
# try newest branch only # try newest branch only
sub = os.path.join(path, str(dirs[-1])) sub = os.path.join(path, str(dirs[-1]))
@@ -298,6 +322,8 @@ class Lim(object):
# make a branch # make a branch
sub = os.path.join(path, str(dirs[-1] + 1)) sub = os.path.join(path, str(dirs[-1] + 1))
bos.mkdir(sub, self.chmod_d) bos.mkdir(sub, self.chmod_d)
if self.chown:
os.chown(sub, self.uid, self.gid)
ret = self.dive(sub, lvs - 1) ret = self.dive(sub, lvs - 1)
if ret is None: if ret is None:
raise Pebkac(500, "rotation bug") raise Pebkac(500, "rotation bug")
@@ -386,20 +412,20 @@ class VFS(object):
self.adot: dict[str, list[str]] = {} self.adot: dict[str, list[str]] = {}
self.js_ls = {} self.js_ls = {}
self.js_htm = "" self.js_htm = ""
self.all_vols: dict[str, VFS] = {} # flattened recursive
self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares
if realpath: if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep) rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
vp = vpath + ("/" if vpath else "") vp = vpath + ("/" if vpath else "")
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.dbpath = self.histpath self.dbpath = self.histpath
self.all_vols = {vpath: self} # flattened recursive self.all_vols[vpath] = self
self.all_nodes = {vpath: self} # also jumpvols/shares self.all_nodes[vpath] = self
self.all_aps = [(rp, [self])] self.all_aps = [(rp, [self])]
self.all_vps = [(vp, self)] self.all_vps = [(vp, self)]
else: else:
self.histpath = self.dbpath = "" self.histpath = self.dbpath = ""
self.all_vols = {}
self.all_nodes = {}
self.all_aps = [] self.all_aps = []
self.all_vps = [] self.all_vps = []
@@ -868,6 +894,53 @@ class VFS(object):
return self 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: if WINDOWS:
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$") re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
@@ -1501,7 +1574,7 @@ class AuthSrv(object):
flags[name] = True flags[name] = True
return 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 name not in zs.split():
if value is True: if value is True:
t = "└─add volflag [{}] = {} ({})" t = "└─add volflag [{}] = {} ({})"
@@ -2077,6 +2150,7 @@ class AuthSrv(object):
all_mte = {} all_mte = {}
errors = False errors = False
free_umask = False free_umask = False
have_reflink = False
for vol in vfs.all_nodes.values(): for vol in vfs.all_nodes.values():
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa: if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True vol.flags["e2ds"] = True
@@ -2114,7 +2188,7 @@ class AuthSrv(object):
if vf not in vol.flags: if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga) vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip nrand tail_who u2abort u2ow ups_who zip_who" zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
for k in zs.split(): for k in zs.split():
if k in vol.flags: if k in vol.flags:
vol.flags[k] = int(vol.flags[k]) vol.flags[k] = int(vol.flags[k])
@@ -2151,8 +2225,17 @@ class AuthSrv(object):
if (is_d and zi != 0o755) or not is_d: if (is_d and zi != 0o755) or not is_d:
free_umask = True free_umask = True
vol.flags.pop("chown", None)
if vol.flags["uid"] != -1 or vol.flags["gid"] != -1:
vol.flags["chown"] = True
vol.flags.pop("fperms", None)
if "chown" in vol.flags or vol.flags.get("chmod_f"):
vol.flags["fperms"] = True
if vol.lim: if vol.lim:
vol.lim.chmod_d = vol.flags["chmod_d"] vol.lim.chmod_d = vol.flags["chmod_d"]
vol.lim.chown = "chown" in vol.flags
vol.lim.uid = vol.flags["uid"]
vol.lim.gid = vol.flags["gid"]
if vol.flags.get("og"): if vol.flags.get("og"):
self.args.uqe = True self.args.uqe = True
@@ -2160,6 +2243,9 @@ class AuthSrv(object):
if "unlistcr" in vol.flags or "unlistcw" in vol.flags: if "unlistcr" in vol.flags or "unlistcw" in vol.flags:
self.args.have_unlistc = True self.args.have_unlistc = True
if "reflink" in vol.flags:
have_reflink = True
zs = str(vol.flags.get("tcolor", "")).lstrip("#") zs = str(vol.flags.get("tcolor", "")).lstrip("#")
if len(zs) == 3: # fc5 => ffcc55 if len(zs) == 3: # fc5 => ffcc55
vol.flags["tcolor"] = "".join([x * 2 for x in zs]) vol.flags["tcolor"] = "".join([x * 2 for x in zs])
@@ -2237,6 +2323,8 @@ class AuthSrv(object):
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s" t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
self.log(t % (vol.vpath, etv), 3) self.log(t % (vol.vpath, etv), 3)
vol.check_landmarks()
# d2d drops all database features for a volume # d2d drops all database features for a volume
for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]: for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]:
if not vol.flags.get(grp, False): if not vol.flags.get(grp, False):
@@ -2522,6 +2610,13 @@ class AuthSrv(object):
t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume." t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
self.log(t + "".join(self.idp_err), 1) self.log(t + "".join(self.idp_err), 1)
if have_reflink:
t = "WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead."
if sys.version_info < (3, 14):
self.log(t % "your python version is not new enough", 1)
if not sys.platform.startswith("linux"):
self.log(t % "your OS is not Linux", 1)
self.vfs = vfs self.vfs = vfs
self.acct = acct self.acct = acct
self.defpw = defpw self.defpw = defpw
@@ -2670,6 +2765,7 @@ class AuthSrv(object):
"def_hcols": list(vf.get("mth") or []), "def_hcols": list(vf.get("mth") or []),
"unlist0": vf.get("unlist") or "", "unlist0": vf.get("unlist") or "",
"see_dots": self.args.see_dots, "see_dots": self.args.see_dots,
"dqdel": self.args.qdel,
"dgrid": "grid" in vf, "dgrid": "grid" in vf,
"dgsel": "gsel" in vf, "dgsel": "gsel" in vf,
"dnsort": "nsort" in vf, "dnsort": "nsort" in vf,
@@ -2690,7 +2786,7 @@ class AuthSrv(object):
"lifetime": vn.js_ls["lifetime"], "lifetime": vn.js_ls["lifetime"],
"u2sort": self.args.u2sort, "u2sort": self.args.u2sort,
} }
vn.js_htm = json.dumps(js_htm) vn.js_htm = json_hesc(json.dumps(js_htm))
vols = list(vfs.all_nodes.values()) vols = list(vfs.all_nodes.values())
if enshare: if enshare:

View File

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

View File

@@ -52,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
"og_no_head", "og_no_head",
"og_s_title", "og_s_title",
"rand", "rand",
"reflink",
"rmagic", "rmagic",
"rss", "rss",
"wo_up_readme", "wo_up_readme",
@@ -113,6 +114,8 @@ def vf_vmap() -> dict[str, str]:
"unlist", "unlist",
"u2abort", "u2abort",
"u2ts", "u2ts",
"uid",
"gid",
"ups_who", "ups_who",
"zip_who", "zip_who",
"zipmaxn", "zipmaxn",
@@ -168,11 +171,14 @@ flagcats = {
"dedup": "enable symlink-based file deduplication", "dedup": "enable symlink-based file deduplication",
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible", "hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible", "hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
"safededup": "verify on-disk data before using it for dedup", "safededup": "verify on-disk data before using it for dedup",
"noclone": "take dupe data from clients, even if available on HDD", "noclone": "take dupe data from clients, even if available on HDD",
"nodupe": "rejects existing files (instead of linking/cloning them)", "nodupe": "rejects existing files (instead of linking/cloning them)",
"chmod_d=755": "unix-permission for new dirs/folders", "chmod_d=755": "unix-permission for new dirs/folders",
"chmod_f=644": "unix-permission for new files", "chmod_f=644": "unix-permission for new files",
"uid=573": "change owner of new files/folders to unix-user 573",
"gid=999": "change owner of new files/folders to unix-group 999",
"sparse": "force use of sparse files, mainly for s3-backed storage", "sparse": "force use of sparse files, mainly for s3-backed storage",
"nosparse": "deny use of sparse files, mainly for slow storage", "nosparse": "deny use of sparse files, mainly for slow storage",
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
@@ -222,6 +228,7 @@ flagcats = {
"d2d": "disables all database stuff, overrides -e2*", "d2d": "disables all database stuff, overrides -e2*",
"hist=/tmp/cdb": "puts thumbnails and indexes at that location", "hist=/tmp/cdb": "puts thumbnails and indexes at that location",
"dbpath=/tmp/cdb": "puts 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", "scan=60": "scan for new files every 60sec, same as --re-maxage",
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso", "nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso", "noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",

View File

@@ -31,6 +31,7 @@ from .util import (
relchk, relchk,
runhook, runhook,
sanitize_fn, sanitize_fn,
set_fperms,
vjoin, vjoin,
wunlink, wunlink,
) )
@@ -262,8 +263,8 @@ class FtpFs(AbstractedFS):
wunlink(self.log, ap, VF_CAREFUL) wunlink(self.log, ap, VF_CAREFUL)
ret = open(fsenc(ap), mode, self.args.iobuf) ret = open(fsenc(ap), mode, self.args.iobuf)
if w and "chmod_f" in vfs.flags: if w and "fperms" in vfs.flags:
os.fchmod(ret.fileno(), vfs.flags["chmod_f"]) set_fperms(ret, vfs.flags)
return ret return ret
@@ -297,8 +298,7 @@ class FtpFs(AbstractedFS):
def mkdir(self, path: str) -> None: def mkdir(self, path: str) -> None:
ap, vfs, _ = self.rv2a(path, w=True) ap, vfs, _ = self.rv2a(path, w=True)
chmod = vfs.flags["chmod_d"] bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
bos.makedirs(ap, chmod) # filezilla expects this
def listdir(self, path: str) -> list[str]: def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path) vpath = join(self.cwd, path)

View File

@@ -33,7 +33,7 @@ except:
from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
from .__version__ import S_VERSION from .__version__ import S_VERSION
from .authsrv import VFS # typechk from .authsrv import LEELOO_DALLAS, VFS # typechk
from .bos import bos from .bos import bos
from .star import StreamTar from .star import StreamTar
from .stolen.qrcodegen import QrCode, qr2svg from .stolen.qrcodegen import QrCode, qr2svg
@@ -79,8 +79,10 @@ from .util import (
hidedir, hidedir,
html_bescape, html_bescape,
html_escape, html_escape,
html_sh_esc,
humansize, humansize,
ipnorm, ipnorm,
json_hesc,
justcopy, justcopy,
load_resource, load_resource,
loadpy, loadpy,
@@ -103,6 +105,7 @@ from .util import (
sanitize_vpath, sanitize_vpath,
sendfile_kern, sendfile_kern,
sendfile_py, sendfile_py,
set_fperms,
stat_resource, stat_resource,
ub64dec, ub64dec,
ub64enc, ub64enc,
@@ -622,6 +625,9 @@ class HttpCli(object):
) or self.args.idp_h_key in self.headers ) or self.args.idp_h_key in self.headers
if trusted_key and trusted_xff: if trusted_key and trusted_xff:
if idp_usr.lower() == LEELOO_DALLAS:
self.loud_reply("send her back", status=403)
return False
self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp) self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp)
else: else:
if not trusted_key: if not trusted_key:
@@ -1110,15 +1116,18 @@ class HttpCli(object):
else: else:
return True return True
host = self.host.lower()
if host.startswith("["):
if "]:" in host:
host = host.split("]:")[0] + "]"
else:
host = host.split(":")[0]
oh = self.out_headers oh = self.out_headers
origin = origin.lower() origin = origin.lower()
good_origins = self.args.acao + [ proto = "https" if self.is_https else "http"
"%s://%s" good_origins = self.args.acao + ["%s://%s" % (proto, host)]
% (
"https" if self.is_https else "http",
self.host.lower().split(":")[0],
)
]
if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins: if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins:
good_origin = True good_origin = True
bad_hdrs = ("",) bad_hdrs = ("",)
@@ -1575,6 +1584,18 @@ class HttpCli(object):
self.log("inaccessible: %r" % ("/" + self.vpath,)) self.log("inaccessible: %r" % ("/" + self.vpath,))
raise Pebkac(401, "authenticate") raise Pebkac(401, "authenticate")
if "quota-available-bytes" in props and not self.args.nid:
bfree, btot, _ = get_df(vn.realpath, False)
if btot:
df = {
"quota-available-bytes": str(bfree),
"quota-used-bytes": str(btot - bfree),
}
else:
df = {}
else:
df = {}
fgen = itertools.chain([topdir], fgen) fgen = itertools.chain([topdir], fgen)
vtop = vjoin(self.args.R, vjoin(vn.vpath, rem)) vtop = vjoin(self.args.R, vjoin(vn.vpath, rem))
@@ -1617,6 +1638,9 @@ class HttpCli(object):
ap = os.path.join(tap, x["vp"]) ap = os.path.join(tap, x["vp"])
pvs["getcontenttype"] = html_escape(guess_mime(rp, ap)) pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
pvs["getcontentlength"] = str(st.st_size) pvs["getcontentlength"] = str(st.st_size)
elif df:
pvs.update(df)
df = {}
for k, v in pvs.items(): for k, v in pvs.items():
if k not in props: if k not in props:
@@ -2068,7 +2092,7 @@ class HttpCli(object):
fdir, fn = os.path.split(fdir) fdir, fn = os.path.split(fdir)
rem, _ = vsplit(rem) rem, _ = vsplit(rem)
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
open_ka: dict[str, Any] = {"fun": open} open_ka: dict[str, Any] = {"fun": open}
open_a = ["wb", self.args.iobuf] open_a = ["wb", self.args.iobuf]
@@ -2126,9 +2150,7 @@ class HttpCli(object):
if nameless: if nameless:
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip()) fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
params = {"suffix": suffix, "fdir": fdir} params = {"suffix": suffix, "fdir": fdir, "vf": vfs.flags}
if "chmod_f" in vfs.flags:
params["chmod"] = vfs.flags["chmod_f"]
if self.args.nw: if self.args.nw:
params = {} params = {}
fn = os.devnull fn = os.devnull
@@ -2177,7 +2199,7 @@ class HttpCli(object):
if self.args.nw: if self.args.nw:
fn = os.devnull fn = os.devnull
else: else:
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
path = os.path.join(fdir, fn) path = os.path.join(fdir, fn)
if not nameless: if not nameless:
self.vpath = vjoin(self.vpath, fn) self.vpath = vjoin(self.vpath, fn)
@@ -2309,7 +2331,7 @@ class HttpCli(object):
if self.args.hook_v: if self.args.hook_v:
log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem) log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
fdir, self.vpath, fn, (vfs, rem) = x fdir, self.vpath, fn, (vfs, rem) = x
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
path2 = os.path.join(fdir, fn) path2 = os.path.join(fdir, fn)
atomic_move(self.log, path, path2, vfs.flags) atomic_move(self.log, path, path2, vfs.flags)
path = path2 path = path2
@@ -2595,7 +2617,7 @@ class HttpCli(object):
dst = vfs.canonical(rem) dst = vfs.canonical(rem)
try: try:
if not bos.path.isdir(dst): if not bos.path.isdir(dst):
bos.makedirs(dst, vfs.flags["chmod_d"]) bos.makedirs(dst, vf=vfs.flags)
except OSError as ex: except OSError as ex:
self.log("makedirs failed %r" % (dst,)) self.log("makedirs failed %r" % (dst,))
if not bos.path.isdir(dst): if not bos.path.isdir(dst):
@@ -2912,6 +2934,7 @@ class HttpCli(object):
ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd) ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
if ok: if ok:
self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
ok, msg = self.get_pwd_cookie(pwd) ok, msg = self.get_pwd_cookie(pwd)
if ok: if ok:
msg = "new password OK" msg = "new password OK"
@@ -2995,12 +3018,20 @@ class HttpCli(object):
# reset both plaintext and tls # reset both plaintext and tls
# (only affects active tls cookies when tls) # (only affects active tls cookies when tls)
for k in ("cppwd", "cppws") if self.is_https else ("cppwd",): for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
ck = gencookie(k, pwd, self.args.R, False) ck = gencookie(k, pwd, self.args.R, self.args.cookie_lax, False)
self.out_headerlist.append(("Set-Cookie", ck)) self.out_headerlist.append(("Set-Cookie", ck))
self.out_headers.pop("Set-Cookie", None) # drop keepalive self.out_headers.pop("Set-Cookie", None) # drop keepalive
else: else:
k = "cppws" if self.is_https else "cppwd" k = "cppws" if self.is_https else "cppwd"
ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly") ck = gencookie(
k,
pwd,
self.args.R,
self.args.cookie_lax,
self.is_https,
dur,
"; HttpOnly",
)
self.out_headers["Set-Cookie"] = ck self.out_headers["Set-Cookie"] = ck
return dur > 0, msg return dur > 0, msg
@@ -3017,6 +3048,9 @@ class HttpCli(object):
self.gctx = vpath self.gctx = vpath
vpath = undot(vpath) vpath = undot(vpath)
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
if "nosub" in vfs.flags:
raise Pebkac(403, "mkdir is forbidden below this folder")
rem = sanitize_vpath(rem, "/") rem = sanitize_vpath(rem, "/")
fn = vfs.canonical(rem) fn = vfs.canonical(rem)
@@ -3030,7 +3064,7 @@ class HttpCli(object):
raise Pebkac(405, 'folder "/%s" already exists' % (vpath,)) raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
try: try:
bos.makedirs(fn, vfs.flags["chmod_d"]) bos.makedirs(fn, vf=vfs.flags)
except OSError as ex: except OSError as ex:
if ex.errno == errno.EACCES: if ex.errno == errno.EACCES:
raise Pebkac(500, "the server OS denied write-access") raise Pebkac(500, "the server OS denied write-access")
@@ -3072,8 +3106,8 @@ class HttpCli(object):
with open(fsenc(fn), "wb") as f: with open(fsenc(fn), "wb") as f:
f.write(b"`GRUNNUR`\n") f.write(b"`GRUNNUR`\n")
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.redirect(vpath, "?edit") self.redirect(vpath, "?edit")
@@ -3147,7 +3181,7 @@ class HttpCli(object):
) )
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/") upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
if not nullwrite: if not nullwrite:
bos.makedirs(fdir_base, vfs.flags["chmod_d"]) bos.makedirs(fdir_base, vf=vfs.flags)
rnd, lifetime, xbu, xau = self.upload_flags(vfs) rnd, lifetime, xbu, xau = self.upload_flags(vfs)
zs = self.uparam.get("want") or self.headers.get("accept") or "" zs = self.uparam.get("want") or self.headers.get("accept") or ""
@@ -3180,7 +3214,7 @@ class HttpCli(object):
if rnd: if rnd:
fname = rand_name(fdir, fname, rnd) fname = rand_name(fdir, fname, rnd)
open_args = {"fdir": fdir, "suffix": suffix} open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
if "replace" in self.uparam: if "replace" in self.uparam:
if not self.can_delete: if not self.can_delete:
@@ -3242,11 +3276,8 @@ class HttpCli(object):
else: else:
open_args["fdir"] = fdir open_args["fdir"] = fdir
if "chmod_f" in vfs.flags:
open_args["chmod"] = vfs.flags["chmod_f"]
if p_file and not nullwrite: if p_file and not nullwrite:
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
# reserve destination filename # reserve destination filename
f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix) f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
@@ -3350,7 +3381,7 @@ class HttpCli(object):
if nullwrite: if nullwrite:
fdir = ap2 = "" fdir = ap2 = ""
else: else:
bos.makedirs(fdir, vfs.flags["chmod_d"]) bos.makedirs(fdir, vf=vfs.flags)
atomic_move(self.log, abspath, ap2, vfs.flags) atomic_move(self.log, abspath, ap2, vfs.flags)
abspath = ap2 abspath = ap2
sz = bos.path.getsize(abspath) sz = bos.path.getsize(abspath)
@@ -3471,8 +3502,8 @@ class HttpCli(object):
ft = "{}:{}".format(self.ip, self.addr[1]) ft = "{}:{}".format(self.ip, self.addr[1])
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg) ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
f.write(ft.encode("utf-8")) f.write(ft.encode("utf-8"))
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
except Exception as ex: except Exception as ex:
suf = "\nfailed to write the upload report: {}".format(ex) suf = "\nfailed to write the upload report: {}".format(ex)
@@ -3523,7 +3554,7 @@ class HttpCli(object):
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
if lim: if lim:
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker) fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
bos.makedirs(fp, vfs.flags["chmod_d"]) bos.makedirs(fp, vf=vfs.flags)
fp = os.path.join(fp, fn) fp = os.path.join(fp, fn)
rem = "{}/{}".format(rp, fn).strip("/") rem = "{}/{}".format(rp, fn).strip("/")
@@ -3591,15 +3622,17 @@ class HttpCli(object):
zs = ub64enc(zb).decode("ascii")[:24].lower() zs = ub64enc(zb).decode("ascii")[:24].lower()
dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs) dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs)
self.log("moving old version to %s/%s" % (dp, mfile2)) self.log("moving old version to %s/%s" % (dp, mfile2))
if bos.makedirs(dp, vfs.flags["chmod_d"]): if bos.makedirs(dp, vf=vfs.flags):
with open(os.path.join(dp, "dir.txt"), "wb") as f: with open(os.path.join(dp, "dir.txt"), "wb") as f:
f.write(afsenc(vrd)) f.write(afsenc(vrd))
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
elif hist_cfg == "s": elif hist_cfg == "s":
dp = os.path.join(mdir, ".hist") dp = os.path.join(mdir, ".hist")
try: try:
bos.mkdir(dp, vfs.flags["chmod_d"]) bos.mkdir(dp, vfs.flags["chmod_d"])
if "chown" in vfs.flags:
bos.chown(dp, vfs.flags["uid"], vfs.flags["gid"])
hidedir(dp) hidedir(dp)
except: except:
pass pass
@@ -3638,8 +3671,8 @@ class HttpCli(object):
wunlink(self.log, fp, vfs.flags) wunlink(self.log, fp, vfs.flags)
with open(fsenc(fp), "wb", self.args.iobuf) as f: with open(fsenc(fp), "wb", self.args.iobuf) as f:
if "chmod_f" in vfs.flags: if "fperms" in vfs.flags:
os.fchmod(f.fileno(), vfs.flags["chmod_f"]) set_fperms(f, vfs.flags)
sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp) sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)
if lim: if lim:
@@ -4865,18 +4898,23 @@ class HttpCli(object):
def tx_svcs(self) -> bool: def tx_svcs(self) -> bool:
aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a" aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a"
ep = self.host ep = self.host
host = ep.split(":")[0] sep = "]:" if "]" in ep else ":"
hport = ep[ep.find(":") :] if ":" in ep else "" if sep in ep:
rip = ( host, hport = ep.rsplit(":", 1)
host hport = ":" + hport
if self.args.rclone_mdns or not self.args.zm else:
else self.conn.hsrv.nm.map(self.ip) or host host = ep
) hport = ""
# safer than html_escape/quotep since this avoids both XSS and shell-stuff
pw = re.sub(r"[<>&$?`\"']", "_", self.pw or "hunter2") if host.endswith(".local") and self.args.zm and not self.args.rclone_mdns:
vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/") rip = self.conn.hsrv.nm.map(self.ip) or host
pw = pw.replace(" ", "%20") if ":" in rip and "[" not in rip:
vp = vp.replace(" ", "%20") rip = "[%s]" % (rip,)
else:
rip = host
vp = (self.uparam["hc"] or "").lstrip("/")
pw = self.pw or "hunter2"
if pw in self.asrv.sesa: if pw in self.asrv.sesa:
pw = "hunter2" pw = "hunter2"
@@ -4885,14 +4923,14 @@ class HttpCli(object):
args=self.args, args=self.args,
accs=bool(self.asrv.acct), accs=bool(self.asrv.acct),
s="s" if self.is_https else "", s="s" if self.is_https else "",
rip=rip, rip=html_sh_esc(rip),
ep=ep, ep=html_sh_esc(ep),
vp=vp, vp=html_sh_esc(vp),
rvp=vjoin(self.args.R, vp), rvp=html_sh_esc(vjoin(self.args.R, vp)),
host=host, host=html_sh_esc(host),
hport=hport, hport=html_sh_esc(hport),
aname=aname, aname=aname,
pw=pw, pw=html_sh_esc(pw),
) )
self.reply(html.encode("utf-8")) self.reply(html.encode("utf-8"))
return True return True
@@ -5041,7 +5079,7 @@ class HttpCli(object):
def setck(self) -> bool: def setck(self) -> bool:
k, v = self.uparam["setck"].split("=", 1) k, v = self.uparam["setck"].split("=", 1)
t = 0 if v in ("", "x") else 86400 * 299 t = 0 if v in ("", "x") else 86400 * 299
ck = gencookie(k, v, self.args.R, False, t) ck = gencookie(k, v, self.args.R, self.args.cookie_lax, False, t)
self.out_headerlist.append(("Set-Cookie", ck)) self.out_headerlist.append(("Set-Cookie", ck))
if "cc" in self.ouparam: if "cc" in self.ouparam:
self.redirect("", "?h#cc") self.redirect("", "?h#cc")
@@ -5053,7 +5091,7 @@ class HttpCli(object):
for k in ALL_COOKIES: for k in ALL_COOKIES:
if k not in self.cookies: if k not in self.cookies:
continue continue
cookie = gencookie(k, "x", self.args.R, False) cookie = gencookie(k, "x", self.args.R, self.args.cookie_lax, False)
self.out_headerlist.append(("Set-Cookie", cookie)) self.out_headerlist.append(("Set-Cookie", cookie))
self.redirect("", "?h#cc") self.redirect("", "?h#cc")
@@ -5556,7 +5594,7 @@ class HttpCli(object):
self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
return True return True
html = self.j2s("rups", this=self, v=jtxt) html = self.j2s("rups", this=self, v=json_hesc(jtxt))
self.reply(html.encode("utf-8"), status=200) self.reply(html.encode("utf-8"), status=200)
return True return True
@@ -5573,7 +5611,7 @@ class HttpCli(object):
db.commit() db.commit()
db.close() db.close()
self.conn.hsrv.broker.ask("reload", False, False).get() self.conn.hsrv.broker.ask("reload", False, True).get()
self.redirect("", "?idp") self.redirect("", "?idp")
return True return True
@@ -5620,15 +5658,15 @@ class HttpCli(object):
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled") raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot create share; please retry in a bit") raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
skey = self.uparam.get("skey") or self.vpath.split("/")[-1]
if self.args.shr_v: if self.args.shr_v:
self.log("handle_eshare: " + self.req) self.log("handle_eshare: " + skey)
cur = idx.get_shr() cur = idx.get_shr()
if not cur: if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...") raise Pebkac(400, "huh, sharing must be disabled in the server config...")
skey = self.vpath.split("/")[-1]
rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall() rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
un = rows[0][0] if rows and rows[0] else "" un = rows[0][0] if rows and rows[0] else ""
@@ -5657,7 +5695,7 @@ class HttpCli(object):
cur.connection.commit() cur.connection.commit()
if reload: 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.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
self.redirect("", "?shares") self.redirect("", "?shares")
@@ -5749,7 +5787,7 @@ class HttpCli(object):
cur.execute(q, (skey, fn)) cur.execute(q, (skey, fn))
cur.connection.commit() 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() self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
fn = quotep(fns[0]) if len(fns) == 1 else "" fn = quotep(fns[0]) if len(fns) == 1 else ""
@@ -6137,13 +6175,13 @@ class HttpCli(object):
self.log("#wow #whoa") self.log("#wow #whoa")
if not self.args.nid: if not self.args.nid:
free, total, _ = get_df(abspath, False) free, total, zs = get_df(abspath, False)
if total is not None: if total:
h1 = humansize(free or 0) h1 = humansize(free or 0)
h2 = humansize(total) h2 = humansize(total)
srv_info.append("{} free of {}".format(h1, h2)) srv_info.append("{} free of {}".format(h1, h2))
elif free is not None: elif zs:
srv_info.append(humansize(free, True) + " free") self.log("diskfree(%r): %s" % (abspath, zs), 3)
srv_infot = "</span> // <span>".join(srv_info) srv_infot = "</span> // <span>".join(srv_info)

View File

@@ -123,6 +123,7 @@ class HttpSrv(object):
self.nm = NetMap([], []) self.nm = NetMap([], [])
self.ssdp: Optional["SSDPr"] = None self.ssdp: Optional["SSDPr"] = None
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403) self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False) self.g422 = Garda(self.args.ban_422, False)

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 "cover" in x[0]] or znil
znil = [x for x in znil if CBZ_01.search(x[0])] or znil znil = [x for x in znil if CBZ_01.search(x[0])] or znil
t = "cbz: %d files, %d hits" % (nf, len(znil)) t = "cbz: %d files, %d hits" % (nf, len(znil))
using = sorted(znil)[0][1].filename
if znil: if znil:
t += ", using " + znil[0][1].filename t += ", using " + using
log(t) log(t)
if not znil: if not znil:
raise Exception("no images inside cbz") raise Exception("no images inside cbz")
fi = zf.open(znil[0][1]) fi = zf.open(using)
else: else:
raise Exception("unknown compression %s" % (pk,)) 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) self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
try: try:
bos.makedirs(ap2, vfs2.flags["chmod_d"]) bos.makedirs(ap2, vf=vfs2.flags)
except: except:
pass pass

View File

@@ -51,6 +51,7 @@ from .util import (
HAVE_PSUTIL, HAVE_PSUTIL,
HAVE_SQLITE3, HAVE_SQLITE3,
HAVE_ZMQ, HAVE_ZMQ,
RE_ANSI,
URL_BUG, URL_BUG,
UTC, UTC,
VERSIONS, VERSIONS,
@@ -60,7 +61,6 @@ from .util import (
HMaccas, HMaccas,
ODict, ODict,
alltrace, alltrace,
ansi_re,
build_netmap, build_netmap,
expat_ver, expat_ver,
gzip, gzip,
@@ -168,6 +168,7 @@ class SvcHub(object):
# for non-http clients (ftp, tftp) # for non-http clients (ftp, tftp)
self.bans: dict[str, int] = {} self.bans: dict[str, int] = {}
self.gpwd = Garda(self.args.ban_pw) self.gpwd = Garda(self.args.ban_pw)
self.gpwc = Garda(self.args.ban_pwc)
self.g404 = Garda(self.args.ban_404) self.g404 = Garda(self.args.ban_404)
self.g403 = Garda(self.args.ban_403) self.g403 = Garda(self.args.ban_403)
self.g422 = Garda(self.args.ban_422, False) self.g422 = Garda(self.args.ban_422, False)
@@ -1408,9 +1409,9 @@ class SvcHub(object):
if self.no_ansi: if self.no_ansi:
fmt = "%s %-21s %s\n" fmt = "%s %-21s %s\n"
if "\033" in msg: if "\033" in msg:
msg = ansi_re.sub("", msg) msg = RE_ANSI.sub("", msg)
if "\033" in src: if "\033" in src:
src = ansi_re.sub("", src) src = RE_ANSI.sub("", src)
elif c: elif c:
if isinstance(c, int): if isinstance(c, int):
msg = "\033[3%sm%s\033[0m" % (c, msg) msg = "\033[3%sm%s\033[0m" % (c, msg)

View File

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

View File

@@ -269,8 +269,8 @@ class ThumbSrv(object):
self.log("joined waiting room for %r" % (tpath,)) self.log("joined waiting room for %r" % (tpath,))
except: except:
thdir = os.path.dirname(tpath) thdir = os.path.dirname(tpath)
chmod = 0o700 if self.args.free_umask else 0o755 chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755
bos.makedirs(os.path.join(thdir, "w"), chmod) bos.makedirs(os.path.join(thdir, "w"), vf=chmod)
inf_path = os.path.join(thdir, "dir.txt") inf_path = os.path.join(thdir, "dir.txt")
if not bos.path.exists(inf_path): if not bos.path.exists(inf_path):

View File

@@ -916,7 +916,7 @@ class Up2k(object):
for vol in vols: for vol in vols:
try: try:
# mkdir gonna happen at snap anyways; # mkdir gonna happen at snap anyways;
bos.makedirs(vol.realpath, vol.flags["chmod_d"]) bos.makedirs(vol.realpath, vf=vol.flags)
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath) dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
except Exception as ex: except Exception as ex:
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
@@ -1379,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" 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) self.log(t % (vol.vpath, rtop), 6)
return True, False 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( n_add, _, _ = self._build_dir(
db, db,
@@ -3305,7 +3309,7 @@ class Up2k(object):
reg, reg,
"up2k._get_volsize", "up2k._get_volsize",
) )
bos.makedirs(ap2, vfs.flags["chmod_d"]) bos.makedirs(ap2, vf=vfs.flags)
vfs.lim.nup(cj["addr"]) vfs.lim.nup(cj["addr"])
vfs.lim.bup(cj["addr"], cj["size"]) vfs.lim.bup(cj["addr"], cj["size"])
@@ -3441,7 +3445,7 @@ class Up2k(object):
"wb", "wb",
fdir=fdir, fdir=fdir,
suffix="-%.6f-%s" % (ts, dip), suffix="-%.6f-%s" % (ts, dip),
chmod=vf.get("chmod_f", -1), vf=vf,
) )
f.close() f.close()
return ret return ret
@@ -3472,6 +3476,8 @@ class Up2k(object):
linked = False linked = False
try: try:
if "reflink" in flags:
raise Exception("reflink")
if not is_mv and not flags.get("dedup"): if not is_mv and not flags.get("dedup"):
raise Exception("dedup is disabled in config") raise Exception("dedup is disabled in config")
@@ -3528,7 +3534,8 @@ class Up2k(object):
linked = True linked = True
except Exception as ex: except Exception as ex:
self.log("cannot link; creating copy: " + repr(ex)) if str(ex) != "reflink":
self.log("cannot link; creating copy: " + repr(ex))
if bos.path.isfile(src): if bos.path.isfile(src):
csrc = src csrc = src
elif fsrc and bos.path.isfile(fsrc): elif fsrc and bos.path.isfile(fsrc):
@@ -4297,7 +4304,7 @@ class Up2k(object):
self.log(t, 1) self.log(t, 1)
raise Pebkac(405, t) raise Pebkac(405, t)
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"]) bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath( c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
svn_dbv.realpath, srem_dbv svn_dbv.realpath, srem_dbv
@@ -4473,7 +4480,10 @@ class Up2k(object):
vp = vjoin(dvp, rem) vp = vjoin(dvp, rem)
try: try:
dvn, drem = self.vfs.get(vp, uname, False, True) dvn, drem = self.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem), dvn.flags["chmod_d"]) dap = dvn.canonical(drem)
bos.mkdir(dap, dvn.flags["chmod_d"])
if "chown" in dvn.flags:
bos.chown(dap, dvn.flags["uid"], dvn.flags["gid"])
except: except:
pass pass
@@ -4543,7 +4553,7 @@ class Up2k(object):
is_xvol = svn.realpath != dvn.realpath is_xvol = svn.realpath != dvn.realpath
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"]) bos.makedirs(os.path.dirname(dabs), vf=dvn.flags)
if is_dirlink: if is_dirlink:
dlabs = absreal(sabs) dlabs = absreal(sabs)
@@ -5055,7 +5065,7 @@ class Up2k(object):
"wb", "wb",
fdir=pdir, fdir=pdir,
suffix="-%.6f-%s" % (job["t0"], dip), suffix="-%.6f-%s" % (job["t0"], dip),
chmod=vf.get("chmod_f", -1), vf=vf,
) )
try: try:
abspath = djoin(pdir, job["tnam"]) abspath = djoin(pdir, job["tnam"])

View File

@@ -155,7 +155,9 @@ except:
HAVE_PSUTIL = False HAVE_PSUTIL = False
try: try:
if os.environ.get("PRTY_NO_MAGIC"): if os.environ.get("PRTY_NO_MAGIC") or (
ANYWIN and not os.environ.get("PRTY_FORCE_MAGIC")
):
raise Exception() raise Exception()
import magic import magic
@@ -241,7 +243,18 @@ except:
BITNESS = struct.calcsize("P") * 8 BITNESS = struct.calcsize("P") * 8
ansi_re = re.compile("\033\\[[^mK]*[mK]") RE_ANSI = re.compile("\033\\[[^mK]*[mK]")
RE_HTML_SH = re.compile(r"[<>&$?`\"';]")
RE_CTYPE = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE)
RE_CDISP = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE)
RE_CDISP_FIELD = re.compile(
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE
)
RE_CDISP_FILE = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
RE_MEMTOTAL = re.compile("^MemTotal:.* kB")
RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
BOS_SEP = ("%s" % (os.sep,)).encode("ascii") BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@@ -486,11 +499,11 @@ def read_ram() -> tuple[float, float]:
with open("/proc/meminfo", "rb", 0x10000) as f: with open("/proc/meminfo", "rb", 0x10000) as f:
zsl = f.read(0x10000).decode("ascii", "replace").split("\n") zsl = f.read(0x10000).decode("ascii", "replace").split("\n")
p = re.compile("^MemTotal:.* kB") p = RE_MEMTOTAL
zs = next((x for x in zsl if p.match(x))) zs = next((x for x in zsl if p.match(x)))
a = int((int(zs.split()[1]) / 0x100000) * 100) / 100 a = int((int(zs.split()[1]) / 0x100000) * 100) / 100
p = re.compile("^MemAvailable:.* kB") p = RE_MEMAVAIL
zs = next((x for x in zsl if p.match(x))) zs = next((x for x in zsl if p.match(x)))
b = int((int(zs.split()[1]) / 0x100000) * 100) / 100 b = int((int(zs.split()[1]) / 0x100000) * 100) / 100
except: except:
@@ -1585,7 +1598,8 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fun = kwargs.pop("fun", open) fun = kwargs.pop("fun", open)
fdir = kwargs.pop("fdir", None) fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None) suffix = kwargs.pop("suffix", None)
chmod = kwargs.pop("chmod", -1) vf = kwargs.pop("vf", None)
fperms = vf and "fperms" in vf
if fname == os.devnull: if fname == os.devnull:
return fun(fname, *args, **kwargs), fname return fun(fname, *args, **kwargs), fname
@@ -1629,11 +1643,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
fp2 = os.path.join(fdir, fp2) fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2: with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8")) f2.write(orig_name.encode("utf-8"))
if chmod >= 0: if fperms:
os.fchmod(f2.fileno(), chmod) set_fperms(f2, vf)
if chmod >= 0: if fperms:
os.fchmod(f.fileno(), chmod) set_fperms(f, vf)
return f, fname return f, fname
@@ -1695,14 +1709,10 @@ class MultipartParser(object):
self.args = args self.args = args
self.headers = http_headers self.headers = http_headers
self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE) self.re_ctype = RE_CTYPE
self.re_cdisp = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE) self.re_cdisp = RE_CDISP
self.re_cdisp_field = re.compile( self.re_cdisp_field = RE_CDISP_FIELD
r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE self.re_cdisp_file = RE_CDISP_FILE
)
self.re_cdisp_file = re.compile(
r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE
)
self.boundary = b"" self.boundary = b""
self.gen: Optional[ self.gen: Optional[
@@ -1974,7 +1984,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
return fn 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: if alg == 1:
zs = "%s %s %s %s" % (salt, fspath, fsize, inode) zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
else: else:
@@ -1984,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") 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( def gen_filekey_dbg(
alg: int, alg: int,
salt: str, salt: str,
@@ -2030,15 +2047,25 @@ def formatdate(ts: Optional[float] = None) -> str:
return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s) return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s)
def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") -> str: def gencookie(
k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = ""
) -> str:
v = v.replace("%", "%25").replace(";", "%3B") v = v.replace("%", "%25").replace(";", "%3B")
if dur: if dur:
exp = formatdate(time.time() + dur) exp = formatdate(time.time() + dur)
else: else:
exp = "Fri, 15 Aug 1997 01:00:00 GMT" exp = "Fri, 15 Aug 1997 01:00:00 GMT"
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax" t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s"
return t % (k, v, r, exp, "; Secure" if tls else "", txt) return t % (
k,
v,
r,
exp,
"; Secure" if tls else "",
txt,
"Lax" if lax else "Strict",
)
def humansize(sz: float, terse: bool = False) -> str: def humansize(sz: float, terse: bool = False) -> str:
@@ -2227,6 +2254,16 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]:
return ret return ret
def html_sh_esc(s: str) -> str:
s = re.sub(RE_HTML_SH, "_", s).replace(" ", "%20")
s = s.replace("\r", "_").replace("\n", "_")
return s
def json_hesc(s: str) -> str:
return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str: def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str:
"""html.escape but also newlines""" """html.escape but also newlines"""
s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@@ -2546,6 +2583,14 @@ def lsof(log: "NamedLogger", abspath: str) -> None:
log("lsof failed; " + min_ex(), 3) log("lsof failed; " + min_ex(), 3)
def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> None:
fno = f.fileno()
if "chmod_f" in vf:
os.fchmod(fno, vf["chmod_f"])
if "chown" in vf:
os.fchown(fno, vf["uid"], vf["gid"])
def _fs_mvrm( def _fs_mvrm(
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
) -> bool: ) -> bool:
@@ -2645,7 +2690,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
return _fs_mvrm(log, abspath, "", False, flags) return _fs_mvrm(log, abspath, "", False, flags)
def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str]: def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
try: try:
ap = fsenc(abspath) ap = fsenc(abspath)
while prune and not os.path.isdir(ap) and BOS_SEP in ap: while prune and not os.path.isdir(ap) and BOS_SEP in ap:
@@ -2656,17 +2701,22 @@ def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str
assert ctypes # type: ignore # !rm assert ctypes # type: ignore # !rm
abspath = fsdec(ap) abspath = fsdec(ap)
bfree = ctypes.c_ulonglong(0) bfree = ctypes.c_ulonglong(0)
btotal = ctypes.c_ulonglong(0)
bavail = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore
ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree) ctypes.c_wchar_p(abspath),
ctypes.pointer(bavail),
ctypes.pointer(btotal),
ctypes.pointer(bfree),
) )
return (bfree.value, None, "") return (bavail.value, btotal.value, "")
else: else:
sv = os.statvfs(ap) sv = os.statvfs(ap)
free = sv.f_frsize * sv.f_bfree free = sv.f_frsize * sv.f_bavail
total = sv.f_frsize * sv.f_blocks total = sv.f_frsize * sv.f_blocks
return (free, total, "") return (free, total, "")
except Exception as ex: except Exception as ex:
return (None, None, repr(ex)) return (0, 0, repr(ex))
if not ANYWIN and not MACOS: if not ANYWIN and not MACOS:
@@ -4147,7 +4197,12 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
stream = codecs.getreader(enc)(stream) stream = codecs.getreader(enc)(stream)
return stream return stream
return open(os.path.join(E.mod, name), mode, encoding=enc) ap = os.path.join(E.mod, name)
if PY2:
return codecs.open(ap, "r", encoding=enc) # type: ignore
return open(ap, mode, encoding=enc)
class Pebkac(Exception): class Pebkac(Exception):

View File

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

View File

@@ -226,6 +226,7 @@ var Ls = {
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', "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_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_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_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort', "ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme', "ct_readme": 'show README.md in folder listings">📜 readme',
@@ -277,6 +278,7 @@ var Ls = {
"ml_drc": "dynamic range compressor", "ml_drc": "dynamic range compressor",
"mt_loop": "loop/repeat one song\">🔁", "mt_loop": "loop/repeat one song\">🔁",
"mt_one": "stop after one song\">1⃣",
"mt_shuf": "shuffle the songs in each folder\">🔀", "mt_shuf": "shuffle the songs in each folder\">🔀",
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶", "mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
"mt_preload": "start loading the next song near the end for gapless playback\">preload", "mt_preload": "start loading the next song near the end for gapless playback\">preload",
@@ -294,6 +296,7 @@ var Ls = {
"mt_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache", "mt_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
"mt_mloop": "loop the open folder\">🔁 loop", "mt_mloop": "loop the open folder\">🔁 loop",
"mt_mnext": "load the next folder and continue\">📂 next", "mt_mnext": "load the next folder and continue\">📂 next",
"mt_mstop": "stop playback\">⏸ stop",
"mt_cflac": "convert flac / wav to opus\">flac", "mt_cflac": "convert flac / wav to opus\">flac",
"mt_caac": "convert aac / m4a to opus\">aac", "mt_caac": "convert aac / m4a to opus\">aac",
"mt_coth": "convert all others (not mp3) to opus\">oth", "mt_coth": "convert all others (not mp3) to opus\">oth",
@@ -850,6 +853,7 @@ var Ls = {
"ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk', "ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk',
"ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯', "ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯',
"ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig', "ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig',
"ct_qdel": 'sletteknappen spør bare én gang om bekreftelse">hurtig🗑',
"ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først', "ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først',
"ct_nsort": 'naturlig sortering (forstår tall i filnavn)">nsort', "ct_nsort": 'naturlig sortering (forstår tall i filnavn)">nsort',
"ct_readme": 'vis README.md nedenfor filene">📜 readme', "ct_readme": 'vis README.md nedenfor filene">📜 readme',
@@ -901,6 +905,7 @@ var Ls = {
"ml_drc": "compressor (volum-utjevning)", "ml_drc": "compressor (volum-utjevning)",
"mt_loop": "spill den samme sangen om og om igjen\">🔁", "mt_loop": "spill den samme sangen om og om igjen\">🔁",
"mt_one": "spill kun én sang\">1⃣",
"mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefølge\">🔀", "mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefølge\">🔀",
"mt_aplay": "forsøk å starte avspilling hvis linken du klikket på for å åpne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres så vil heller ikke nettside-URLen bli oppdatert med sang-ID'er når musikk spilles, i tilfelle innstillingene skulle gå tapt og nettsiden lastes på ny\">a▶", "mt_aplay": "forsøk å starte avspilling hvis linken du klikket på for å åpne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres så vil heller ikke nettside-URLen bli oppdatert med sang-ID'er når musikk spilles, i tilfelle innstillingene skulle gå tapt og nettsiden lastes på ny\">a▶",
"mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles", "mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles",
@@ -918,6 +923,7 @@ var Ls = {
"mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">oppfrisk", "mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">oppfrisk",
"mt_mloop": "repeter hele mappen\">🔁 gjenta", "mt_mloop": "repeter hele mappen\">🔁 gjenta",
"mt_mnext": "hopp til neste mappe og fortsett\">📂 neste", "mt_mnext": "hopp til neste mappe og fortsett\">📂 neste",
"mt_mstop": "stopp avspilling\">⏸ stopp",
"mt_cflac": "konverter flac / wav-filer til opus\">flac", "mt_cflac": "konverter flac / wav-filer til opus\">flac",
"mt_caac": "konverter aac / m4a-filer til to opus\">aac", "mt_caac": "konverter aac / m4a-filer til to opus\">aac",
"mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre", "mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre",
@@ -1474,6 +1480,7 @@ var Ls = {
"ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL', "ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL',
"ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动', "ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动',
"ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件', "ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件',
"ct_qdel": '删除文件时,只需确认一次">快删', //m
"ct_dir1st": '在文件之前排序文件夹">📁 排序', "ct_dir1st": '在文件之前排序文件夹">📁 排序',
"ct_nsort": '正确排序以数字开头的文件名">数字排序', //m "ct_nsort": '正确排序以数字开头的文件名">数字排序', //m
"ct_readme": '在文件夹列表中显示 README.md">📜 readme', "ct_readme": '在文件夹列表中显示 README.md">📜 readme',
@@ -1525,6 +1532,7 @@ var Ls = {
"ml_drc": "动态范围压缩器", "ml_drc": "动态范围压缩器",
"mt_loop": "循环播放当前的歌曲\">🔁", //m "mt_loop": "循环播放当前的歌曲\">🔁", //m
"mt_one": "只播放一首歌后停止\">1⃣", //m
"mt_shuf": "在每个文件夹中随机播放歌曲\">🔀", "mt_shuf": "在每个文件夹中随机播放歌曲\">🔀",
"mt_aplay": "如果链接中有歌曲 ID则自动播放,禁用此选项将停止在播放音乐时更新页面 URL 中的歌曲 ID以防止在设置丢失但 URL 保留时自动播放\">自动播放▶", "mt_aplay": "如果链接中有歌曲 ID则自动播放,禁用此选项将停止在播放音乐时更新页面 URL 中的歌曲 ID以防止在设置丢失但 URL 保留时自动播放\">自动播放▶",
"mt_preload": "在歌曲快结束时开始加载下一首歌,以实现无缝播放\">预加载", "mt_preload": "在歌曲快结束时开始加载下一首歌,以实现无缝播放\">预加载",
@@ -1542,6 +1550,7 @@ var Ls = {
"mt_uncache": "清除缓存&nbsp;$N如果你的浏览器缓存了一个损坏的歌曲副本而拒绝播放请尝试此操作\">uncache", "mt_uncache": "清除缓存&nbsp;$N如果你的浏览器缓存了一个损坏的歌曲副本而拒绝播放请尝试此操作\">uncache",
"mt_mloop": "循环打开的文件夹\">🔁 循环", "mt_mloop": "循环打开的文件夹\">🔁 循环",
"mt_mnext": "加载下一个文件夹并继续\">📂 下一首", "mt_mnext": "加载下一个文件夹并继续\">📂 下一首",
"mt_mstop": "停止播放\">⏸ 停止", //m
"mt_cflac": "将 flac / wav 转换为 opus\">flac", "mt_cflac": "将 flac / wav 转换为 opus\">flac",
"mt_caac": "将 aac / m4a 转换为 opus\">aac", "mt_caac": "将 aac / m4a 转换为 opus\">aac",
"mt_coth": "将所有其他(不是 mp3转换为 opus\">oth", "mt_coth": "将所有其他(不是 mp3转换为 opus\">oth",
@@ -1875,9 +1884,635 @@ var Ls = {
"lang_set": "刷新以使更改生效?", "lang_set": "刷新以使更改生效?",
}, },
"deu": {
"tt": "Deutsch",
"cols": {
"c": "Aktionen",
"dur": "Dauer",
"q": "Qualität / Bitrate",
"Ac": "Audiocodec",
"Vc": "Videocodec",
"Fmt": "Format / Container",
"Ahash": "Audio Checksumme",
"Vhash": "Video Checksumme",
"Res": "Auflösung",
"T": "Dateityp",
"aq": "Audioqualität / Bitrate",
"vq": "Videoqualität / Bitrate",
"pixfmt": "Subsampling / Pixelstruktur",
"resw": "horizontale Auflösung",
"resh": "vertikale Auflösung",
"chs": "Audiokanäle",
"hz": "Abtastrate"
},
"hks": [
[
"misc",
["ESC", "Dinge schliessen"],
"file-manager",
["G", "zwischen Liste und Gitter wechseln"],
["T", "zwischen Vorschaubildern und Symbolen wechseln"],
["⇧ A/D", "Vorschaubildergrösse ändern"],
["STRG-K", "Auswahl löschen"],
["STRG-X", "Auswahl ausschneiden"],
["STRG-C", "Auswahl in Zwischenablage kopieren"],
["STRG-V", "Zwischenablage hier einfügen"],
["Y", "Auswahl herunterladen"],
["F2", "Auswahl umbenennen"],
"file-list-sel",
["LEER", "Dateiauswahl aktivieren"],
["↑/↓", "Cursor verschieben"],
["STRG ↑/↓", "Cursor und Bildschirm verschieben"],
["⇧ ↑/↓", "Vorherige / nächste Datei auswählen"],
["STRG-A", "Alle Dateien / Ordner auswählen"],
], [
"navigation",
["B", "Zwischen Brotkrumen und Navpane wechseln"],
["I/K", "vorheriger / nächster Ordner"],
["M", "übergeordneter Ordner (oder Vorherigen einklappen)"],
["V", "Zwischen Textdateien und Navpane wechseln"],
["A/D", "Grösse der Navpane ändern"],
], [
"audio-player",
["J/L", "Vorheriger / nächster Song"],
["U/O", "10 Sek. vor- / zurückspringen"],
["0..9", "zu 0%..90% springen"],
["P", "Wiedergabe / Pause"],
["S", "aktuell abgespielten Song auswählen"],
["Y", "Sing herunterladen"],
], [
"image-viewer",
["J/L, ←/→", "vorheriges / nächstes Bild"],
["Pos1/Ende", "erstes / letztes Bild"],
["F", "Vollbild"],
["R", "im Uhrzeigersinn drehen"],
["⇧ R", "gegen den Uhrzeigensinn drehen"],
["S", "Bild auswählen"],
["Y", "Bild herunterladen"],
], [
"video-player",
["U/O", "10 Sek. vor- / zurückspringen"],
["P/K/LEER", "Wiedergabe / Pause"],
["C", "continue playing next"],
["V", "Wiederholungs-Wiedergabe (Loop)"],
["M", "Stummschalten"],
["[ und ]", "Loop-Interval einstellen"],
], [
"textfile-viewer",
["I/K", "vorherige / nächste Datei"],
["M", "Textdatei schliessen"],
["E", "Textdatei bearbeiten"],
["S", "Textdatei auswählen (für Ausschneiden / Kopieren / Umbenennen)"],
]
],
"m_ok": "OK",
"m_ng": "Abbrechen",
"enable": "Aktivieren",
"danger": "ACHTUNG",
"clipped": "in Zwischenablage kopiert",
"ht_s1": "Sekunde",
"ht_s2": "Sekunden",
"ht_m1": "Minute",
"ht_m2": "Minuten",
"ht_h1": "Stunde",
"ht_h2": "Stunden",
"ht_d1": "Tag",
"ht_d2": "Tage",
"ht_and": " und ",
"goh": "Einstellungen",
"gop": 'zum vorherigen Ordner springen">vorh.',
"gou": 'zum übergeordneter Ordner springen">hoch',
"gon": 'zum nächsten Ordner springen">nächst.',
"logout": "Abmelden ",
"access": " Zugriff",
"ot_close": "Submenu schliessen",
"ot_search": "Dateien nach Attributen, Pfad/Name, Musiktags oder beliebiger Kombination suchen$N$N&lt;code&gt;foo bar&lt;/code&gt; = muss «foo» und «bar» enthalten,$N&lt;code&gt;foo -bar&lt;/code&gt; = muss «foo» aber nicht «bar» enthalten,$N&lt;code&gt;^yana .opus$&lt;/code&gt; = beginnt mit «yana» und ist «opus»-Datei$N&lt;code&gt;&quot;try unite&quot;&lt;/code&gt; = genau «try unite» enthalten$N$NDatumsformat ist iso-8601, z.B.$N&lt;code&gt;2009-12-31&lt;/code&gt; oder &lt;code&gt;2020-09-12 23:30:00&lt;/code&gt;",
"ot_unpost": "unpost: lösche deine letzten Uploads oder breche unvollständige ab",
"ot_bup": "bup: Basic Uploader, unterstützt sogar Neuheiten wie Netscape 4.0",
"ot_mkdir": "mkdir: Neuen Ordner erstellen",
"ot_md": "new-md: Neues Markdown-Dokument erstellen",
"ot_msg": "msg: Eine Nachricht an das Server-Log schicken",
"ot_mp": "Media Player-Optionen",
"ot_cfg": "Konfigurationsoptionen",
"ot_u2i": 'up2k: Dateien hochladen (wenn du Schreibrechte hast) oder in den Suchmodus wechseln, um zu prüfen, ob sie bereits auf dem Server existieren$N$NUploads sind fortsetzbar, multithreaded und behalten Dateizeitstempel, verbrauchen aber mehr CPU als [🎈]&nbsp; (der einfache Uploader)<br /><br />während Uploads wird dieses Symbol zu einem Fortschrittsanzeiger!',
"ot_u2w": 'up2k: Dateien mit Wiederaufnahme-Unterstützung hochladen (Browser schließen und später dieselben Dateien erneut hochladen)$N$Nmultithreaded, behält Dateizeitstempel, verbraucht aber mehr CPU als [🎈]&nbsp; (der einfache Uploader)<br /><br />während Uploads wird dieses Symbol zu einem Fortschrittsanzeiger!',
"ot_noie": 'Bitte benutze Chrome / Firefox / Edge',
"ab_mkdir": "Ordner erstellen",
"ab_mkdoc": "Markdown Doc erstellen",
"ab_msg": "Nachricht an Server Log senden",
"ay_path": "zu Ordnern springen",
"ay_files": "zu Dateien springen",
"wt_ren": "ausgewählte Elemente umbenennen$NHotkey: F2",
"wt_del": "ausgewählte Elemente löschen$NHotkey: STRG-K",
"wt_cut": "ausgewählte Elemente ausschneiden &lt;small&gt;(um sie dann irgendwo anders einzufügen)&lt;/small&gt;$NHotkey: STRG-X",
"wt_cpy": "ausgewählte Elemente in Zwischenablage kopieren$N(um sie dann irgendwo anders einzufügen)$NHotkey: ctrl-C",
"wt_pst": "zuvor ausgeschnittenen / kopierte Elemente einfügen$NHotkey: STRG-V",
"wt_selall": "alle Dateien auswählen$NHotkey: STRG-A (wenn Datei fokusiert)",
"wt_selinv": "Auswahl invertieren",
"wt_zip1": "Diesen Ordner als Archiv herunterladen",
"wt_selzip": "Auswahl als Archiv herunterladen",
"wt_seldl": "Auswahl als separate Dateien herunterladen$NHotkey: Y",
"wt_npirc": "kopiere Titelinfo als IRC-formattierten Text",
"wt_nptxt": "kopiere Titelinfo als Text",
"wt_m3ua": "Zu M3U-Wiedergabeliste hinzufügen (wähle später <code>📻copy</code>)",
"wt_m3uc": "M3U-Wiedergabeliste in Zwischenablage kopieren",
"wt_grid": "Zwischen Gitter und Liste wechseln$NHotkey: G",
"wt_prev": "Vorheriger Titel$NHotkey: J",
"wt_play": "Wiedergabe / Pause$NHotkey: P",
"wt_next": "Nächster Titel$NHotkey: L",
"ul_par": "Parallele Uploads:",
"ut_rand": "Zufällige Dateinamen",
"ut_u2ts": "Zuletzt geändert-Zeitstempel von$Ndeinem Dateisystem auf den Server übertragen\">📅",
"ut_ow": "Existierende Dateien auf dem Server überschreiben?$N🛡: Nie (generiert einen neuen Dateinamen)$N🕒: Überschreiben, wenn Server-Datei älter ist als meine$N♻: Überschreiben, wenn der Dateiinhalt anders ist",
"ut_mt": "Andere Dateien während des Uploads hashen$N$Nsolltest du deaktivieren, falls deine CPU oder Festplatte zum Flaschenhals werden könnte",
"ut_ask": 'Vor dem Upload nach Bestätigung fragen">💭',
"ut_pot": "Verbessert Upload-Geschwindigkeit$Nindem das UI weniger komplex gemacht wird",
"ut_srch": "nicht wirklich hochladen, stattdessen prüfen ob Datei bereits auf dem Server existiert (scannt alle Ordner, die du lesen kannst)",
"ut_par": "setze auf 0 zum Pausieren$N$Nerhöhe, wenn deine Verbindung langsam / instabil ist$N$lass auf 1 im LAN oder wenn die Festplatte auf dem Server ein Flaschenhals ist",
"ul_btn": "Dateien / Ordner hier<br>ablegen (oder klick mich)",
"ul_btnu": "U P L O A D",
"ul_btns": "S U C H E N",
"ul_hash": "hash",
"ul_send": "senden",
"ul_done": "fertig",
"ul_idle1": "keine Uploads in der Warteschlange",
"ut_etah": "durchschnittl. &lt;em&gt;hashing&lt;/em&gt; Geschw. & gesch. Restzeit",
"ut_etau": "durchschnittl. &lt;em&gt;upload&lt;/em&gt; Geschw. & gesch. Restzeit",
"ut_etat": "durchschnittl. &lt;em&gt;total&lt;/em&gt; Geschw. & gesch. Restzeit",
"uct_ok": "Erfolgreich abgeschlossen",
"uct_ng": "no-good: fehlgeschlagen / abgelehnt / nicht gefunden",
"uct_done": "ok and ng zusammen",
"uct_bz": "wird gehasht oder hochgeladen",
"uct_q": "ausstehend",
"utl_name": "Dateiname",
"utl_ulist": "Liste",
"utl_ucopy": "kopieren",
"utl_links": "Links",
"utl_stat": "Status",
"utl_prog": "Fortschritt",
// keep short:
"utl_404": "404",
"utl_err": "Fehler",
"utl_oserr": "OS-Fehler",
"utl_found": "gefunden",
"utl_defer": "zurückstellen",
"utl_yolo": "YOLO",
"utl_done": "fertig",
"ul_flagblk": "Die Dateien wurden zur Warteschlange hinzugefügt</b><br>jedoch ist up2k gerade in einem anderen Browsertab aktiv.<br>Ich warte, bis der Upload abgeschlossen ist.",
"ul_btnlk": "Die Serverkonfiguration hat diese Einstellung gesperrt",
"udt_up": "Upload",
"udt_srch": "Suchen",
"udt_drop": "hier ablegen",
"u_nav_m": '<h6>okay, was gibts??</h6><code>Eingabe</code> = Dateien (1 oder mehr)\n<code>ESC</code> = 1 Ordner (inkl. Unterordner)',
"u_nav_b": '<a href="#" id="modal-ok">Dateien</a><a href="#" id="modal-ng">1 Ordner</a>',
"cl_opts": "Schalter",
"cl_themes": "Themes",
"cl_langs": "Sprache",
"cl_ziptype": "Ordner Download",
"cl_uopts": "up2k Schalter",
"cl_favico": "Favicon",
"cl_bigdir": "grosse Ordner",
"cl_hsort": "#sort",
"cl_keytype": "Schlüsselnotation",
"cl_hiddenc": "Spalten verstecken",
"cl_hidec": "verstecken",
"cl_reset": "zurücksetzen",
"cl_hpick": "zum Verstecken, tippe auf Spaltenüberschriften in der Tabelle unten",
"cl_hcancel": "Spaltenbearbeitung abgebrochen",
"ct_grid": '田 Das Raster&trade;',
"ct_ttips": '◔ ◡ ◔"> Tooltips',
"ct_thumb": 'In Raster-Ansicht, zwischen Icons und Vorschau wechseln$NHotkey: T">🖼️ Vorschaubilder',
"ct_csel": 'Benutze STRG und UMSCHALT für Dateiauswahl in Raster-Ansicht">sel',
"ct_ihop": 'Wenn die Bildanzeige geschlossen ist, scrolle runter zu den zuletzt angesehenen Dateien">g⮯',
"ct_dots": 'Verstecke Dateien anzeigen (wenn erlaubt durch Server)">dotfiles',
"ct_qdel": 'Nur einmal fragen, wenn mehrere Dateien gelöscht werden">qdel',
"ct_dir1st": 'Ordner vor Dateien sortieren">📁 zuerst',
"ct_nsort": 'Natürliche Sortierung (für Dateinamen mit führenden Ziffern)">nsort',
"ct_readme": 'README.md in Dateiliste anzeigen">📜 readme',
"ct_idxh": 'index.html anstelle von Dateiliste anzeigen">htm',
"ct_sbars": 'Scrollbars zeigen">⟊',
"cut_umod": "Sollte die Datei bereits auf dem Server existieren, den 'Zuletzt geändert'-Zeitstempel an deine lokale Datei anpassen (benötigt Lese- und Löschrechte)\">re📅",
"cut_turbo": "der YOLO-Knopf, den du wahrscheinlich NICHT aktivieren willst:$N$NBenutze ihn, falls du ne Menge Zeug hochladen wolltest und aus irgendeinem Grund neustarten musstest und du so schnell wie möglich weitermachen willst.$N$Ndies ersetzt den Hash-Check mit einem einfachen <em>&quot;Ist die Datei auf dem Server gleich gross?&quot;</em>, wenn die Datei also anderen Inhalt hat, wird sie NICHT nochmal hochgeladen!$N$NDu solltest dieses Feature ausschalten, sobald der Upload fertig ist und dann die gleichen Dateien nochmal &quot;hochladen&quot;, damit der Client sie verifizieren kann.\">turbo",
"cut_datechk": "Funktioniert nur in kombination mit dem Turbo-Knopf$N$NReduziert den YOLO-Faktor ein bisschen; prüft, ob der Zeitstempel deiner Datei mit dem auf dem Server übereinstimmt$N$Nsollte <em>theoretisch</em> die meisten unfertigen / korrupten Uploads erwischen, ist aber nicht zu gebrauchen, um einen Prüfdurchgang nach einem Turbo-Upload zu machen\">date-chk",
"cut_u2sz": "Grösse (in MiB) für jeden Upload-Chunk; mit grossen Werten fliegen die Bits besser über den Atlantik. Versuche kleine Werte, wenn du eine schlechte Verbindung hast (z.B. du benutzt mobile Daten in Deutschland)",
"cut_flag": "Stelle sicher, dass nur ein Tab auf einmal Dateien hochlädt$N -- andere Tabs müssen diese Funktion auch aktiviert haben $N -- funktioniert nur bei Tabs mit der gleichen Domäne",
"cut_az": "Lädt Dateien in alphabetischer Reihenfolge hoch, anstatt nach Dateigrösse$N$NAlphabethische Reihenfolge kann es einfacher machen, Server-Fehler mit naktem Auge zu erkennen, macht aber Uploads über Glassfaser / LAN etwas langsamer",
"cut_nag": "Benachrichtigung über das Betriebssystem abgeben, wenn Upload fertig ist$N(nur wenn Browser oder Tab nicht im Vordergrund ist)",
"cut_sfx": "Spielt ein Ton ab, wenn Upload fertig ist$N(nur wenn Browser oder Tab nicht im Vordergrund ist)",
"cut_mt": "Multithreading benutzen um Datei-Hashing zu beschleunigen$N$NDies nutzt Web-Workers und benötigt$Nmehr RAM (bis zu 512 MiB extra)$N$Nbeschleunigt HTTPS 30% schneller, HTTP um 4.5x\">mt",
"cut_wasm": "benutzt WASM anstelle des Browser-eigenen Hashers; verbessert Geschwindigkeit auf Chromium-basierten Browsern, erhöht aber die CPU-Auslastung. Viele ältere Versionen von Chrome haben Memory-Leaks, die den gesamten RAM verbrauchen und dann crashen, wenn diese Funktion aktiviert ist.\">wasm",
"cft_text": "Favicon Text (leer lassen und neuladen zum Deaktivieren)",
"cft_fg": "Vordergrundfarbe",
"cft_bg": "Hintergrundfarbe",
"cdt_lim": "max. Anz. Dateien, die in einem Ordner gezeigt werden sollen",
"cdt_ask": "beim Runterscrollen nach $NAktion fragen statt mehr,$NDateien zu laden",
"cdt_hsort": "Menge an Sortierregeln (&lt;code&gt;,sorthref&lt;/code&gt;) in Media-URLs enthalten sein sollen. Ein Wert von 0 sorgt dafür, dass Sortierregeln in Media-URLs ignoriert werden",
"tt_entree": "Navpane anzeigen (Ordnerbaum Sidebar)$NHotkey: B",
"tt_detree": "Breadcrumbs anzeigen$NHotkey: B",
"tt_visdir": "zu ausgewähltem Ordner scrollen",
"tt_ftree": "zw. Ordnerbaum / Textdateien wechseln$NHotkey: V",
"tt_pdock": "übergeordnete Ordner in einem angedockten Fenster oben anzeigen",
"tt_dynt": "autom. wachsen wenn Baum wächst",
"tt_wrap": "Zeilenumbruch",
"tt_hover": "Beim Hovern überlange Zeilen anzeigen$N(Scrollen funktioniert nicht ausser $N&nbsp; Cursor ist im linken Gutter)",
"ml_pmode": "am Ende des Ordners...",
"ml_btns": "cmds",
"ml_tcode": "transcodieren",
"ml_tcode2": "transcodieren zu",
"ml_tint": "färben",
"ml_eq": "Audio Equalizer",
"ml_drc": "Dynamic Range Compressor",
"mt_loop": "Song wiederholen\">🔁",
"mt_one": "Wiedergabe nach diesem Song beenden\">1⃣",
"mt_shuf": "Zufällige Wiedergabe im Ordner\">🔀",
"mt_aplay": "automatisch abspielen, wenn der Link, mit dem du auf den Server zugreifst, eine Titel-ID enthält$N$NDeaktivieren verhindert auch, dass die Seiten-URL bei Musikwiedergabe mit Titel-IDs aktualisiert wird, um Autoplay zu verhindern, falls diese Einstellungen verloren gehen, die URL aber bestehen bleibt\">a▶",
"mt_preload": "nächsten Titel gegen Ende vorladen für nahtlose Wiedergabe\">Vorladen",
"mt_prescan": "vor Ende des letzten Titels zum nächsten Ordner wechseln,$Ndamit der Browser die$NWiedergabe nicht stoppt\">Navigation",
"mt_fullpre": "versuchen, den gesamten Titel vorzuladen;$N✅ bei <b>unzuverlässiger</b> Verbindung aktivieren,$N❌ bei langsamer Verbindung deaktivieren\">vollst&auml;ndig",
"mt_fau": "auf Handys verhindern, dass Musik stoppt, wenn der nächste Titel nicht schnell genug vorlädt (kann zu fehlerhafter Tag-Anzeige führen)\">☕️",
"mt_waves": "Wellenform-Suchleiste:$NAudio-Amplitude in der Leiste anzeigen\">~s",
"mt_npclip": "Buttons zum Kopieren des aktuellen Titels anzeigen\">/np",
"mt_m3u_c": "Buttons zum Kopieren der$Nausgewählten Titel als m3u8-Wiedergabeliste anzeigen\">📻",
"mt_octl": "OS-Integration (Media-Hotkeys/OSD)\">os-ctl",
"mt_oseek": "Suchen via OS-Integration erlauben$N$NHinweis: auf einigen Geräten (iPhones)$Nersetzt dies den nächsten-Titel-Button\">Suchen",
"mt_oscv": "Albumcover in OSD anzeigen\">Cover",
"mt_follow": "den spielenden Titel im Blick behalten\">🎯",
"mt_compact": "kompakte Steuerelemente\">⟎",
"mt_uncache": "Cache leeren &nbsp;(probier das, wenn dein Browser$Neine defekte Kopie eines Titels zwischenspeichert und sich weigert, ihn abzuspielen)\">Cache leeren",
"mt_mloop": "offenen Ordner wiederholen\">🔁 Schleife",
"mt_mnext": "nächsten Ordner laden und fortfahren\">📂 nächster",
"mt_mstop": "Wiedergabe beenden\">⏸ Stop",
"mt_cflac": "FLAC / WAV zu OPUS konvertierebn\">flac",
"mt_caac": "AAC / M4A zu OPUS konvertieren\">aac",
"mt_coth": "Convertiere alle Dateien (die nicht MP3 sind) zu OPUS\">oth",
"mt_c2opus": "Beste Wahl für Desktops, Laptops, Android\">opus",
"mt_c2owa": "opus-weba, für iOS 17.5 und neuer\">owa",
"mt_c2caf": "opus-caf, für iOS 11 bis 17\">caf",
"mt_c2mp3": "benutze dieses Format für ältere Geräte\">mp3",
"mt_c2ok": "Gute Wahl, Chef!",
"mt_c2nd": "Das ist nicht das empfohlene Ausgabeformat für dein Gerät, aber passt schon",
"mt_c2ng": "Dein Gerät scheint dieses Ausgabeformat nicht zu unterstützen, aber lass trotzdem mal probieren",
"mt_xowa": "Es gibt Bugs in iOS, die die Hintergrund-Wiedergabe mit diesem Format verhindern; bitte nutze caf oder mp3 stattdessen",
"mt_tint": "Hintergrundlevel (0-100) auf der Seekbar$Num Buffern weniger ablenkend zu machen",
"mt_eq": "Aktiviert Equalizer und Lautstärkeregelung;$N$Nboost &lt;code&gt;0&lt;/code&gt; = Standard 100% Lautstärke (unverändert)$N$Nwidth &lt;code&gt;1 &nbsp;&lt;/code&gt; = Standard Stereo (unverändert)$Nwidth &lt;code&gt;0.5&lt;/code&gt; = 50% Links-Rechts-Crossfeed$Nwidth &lt;code&gt;0 &nbsp;&lt;/code&gt; = Mono$N$Nboost &lt;code&gt;-0.8&lt;/code&gt; &amp; width &lt;code&gt;10&lt;/code&gt; = Gesangsentfernung :^)$N$NDer Equalizer macht nahtlose Alben vollständig nahtlos, also lass' ihn mit allen Werten auf Null (außer width = 1) aktiviert, wenn dir das wichtig ist",
"mt_drc": "Aktiviert den Dynamic Range Compressor (Lautstärkeglättung/-begrenzung); aktiviert auch den Equalizer zum Ausgleich, setze alle EQ-Felder außer 'width' auf 0, wenn du das nicht willst$N$Nsenkt die Lautstärke von Audio über SCHWELLENWERT dB; für jedes VERHÄLTNIS dB über SCHWELLENWERT gibt es 1 dB Ausgabe, also bedeuten Standardwerte von tresh -24 und ratio 12, dass es nie lauter als -22 dB werden sollte und der Equalizer-Boost sicher auf 0.8 oder sogar 1.8 mit ATK 0 und einem großen RLS wie 90 erhöht werden kann (funktioniert nur in Firefox; in anderen Browsern ist RLS max. 1)$N$N(siehe Wikipedia, dort wird es viel besser erklärt)",
"mb_play": "Abspielen",
"mm_hashplay": "Diese Audiodatei abspielen?",
"mm_m3u": "Drücke <code>Eingabe/OK</code> zum Abspielen\nDrücke <code>ESC/Abbrechen</code> zum Bearbeiten",
"mp_breq": "Benötigt Firefox 82+ oder Chrome 73+ oder iOS 15+",
"mm_bload": "Lädt...",
"mm_bconv": "Konvertiere zu {0}, bitte warte...",
"mm_opusen": "Dein Browser kann AAC- / M4A-Dateien nicht abspielen;\nUmwandlung zu Opus ist jetzt aktiv",
"mm_playerr": "Wiedergabefehler: ",
"mm_eabrt": "Der Wiedergabeversuch wurde abgebrochen",
"mm_enet": "Dein Internet läuft auf Edge, wa?",
"mm_edec": "Die Datei scheint beschädigt zu sein??",
"mm_esupp": "Dein Browser versteht dieses Audioformat nicht",
"mm_eunk": "Unbekannter Fehler",
"mm_e404": "Konnte Datei nicht abspielen; Fehler 404: Datei nicht gefunden.",
"mm_e403": "Konnte Datei nicht abspielen; Fehler 403: Zugriff verweigert.\n\nDrücke F5 zum Neuladen, vielleicht wurdest du abgemeldet",
"mm_e500": "Konnte Datei nicht abspielen; Fehler 500: Prüfe die Serverlogs.",
"mm_e5xx": "Konnte Datei nicht abspielen; Server Fehler ",
"mm_nof": "finde keine weiteren Audiodateien in der Nähe",
"mm_prescan": "Suche nach Musik zum Abspielen...",
"mm_scank": "Nächster Song gefunden:",
"mm_uncache": "Cache geleert; Alle Songs werden beim nächsten Abspielversuch neu heruntergeladen",
"mm_hnf": "dieser Song existiert nicht mehr",
"im_hnf": "dieses Bild existiert nicht mehr",
"f_empty": 'Dieser Ordner ist leer',
"f_chide": 'Dies blendet die Spalte «{0}» aus\n\nDu kannst Spalten in den Einstellungen wieder einblenden.',
"f_bigtxt": "Diese Datei ist {0} MiB gross -- Sicher, dass du sie als Text anzeigen willst?",
"f_bigtxt2": "Möchtest du stattdessen nur das Ende der Datei anzeigen? Das aktiviert ausserdem die Folgen- und Verfolgen-Funktion, welche neu hinzugefügte Textzeilen in Echtzeit anzeigt",
"fbd_more": '<div id="blazy">zeige <code>{0}</code> von <code>{1}</code> Dateien; <a href="#" id="bd_more">{2} anzeigen</a> oder <a href="#" id="bd_all">alle anzeigen</a></div>',
"fbd_all": '<div id="blazy">zeige <code>{0}</code> von <code>{1}</code> Dateien; <a href="#" id="bd_all">alle anzeigen</a></div>',
"f_anota": "nur {0} der {1} Elemente wurden ausgewählt;\num den gesamten Ordner auszuwählen, zuerst nach unten scrollen",
"f_dls": 'die Dateilinks im aktuellen Ordner wurden\nin Downloadlinks geändert',
"f_partial": "Um eine Datei sicher herunterzuladen, die gerade hochgeladen wird, klicke bitte die Datei mit dem gleichen Namen, aber ohne die <code>.PARTIAL</code>-Endung. Bitte drücke Abbrechen oder Escape, um dies zu tun.\n\nWenn du auf OK / Eingabe drückst, ignorierst du diese Warnung und lädst die <code>.PARTIAL</code>-Datei herunter, die ziemlich sicher beschädigte Daten enthält.",
"ft_paste": "{0} Elemente einfügen$NHotkey: STRG-V",
"fr_eperm": 'Umbenennen fehlgeschlagen:\nDir fehlt die "Verschieben"-Berechtigung in diesem Ordner',
"fd_eperm": 'Löschen fehlgeschlagen:\nDir fehlt die "Löschen"-Berechtigung in diesem Ordner',
"fc_eperm": 'Ausschneiden fehlgeschlagen:\nDir fehlt die "Verschieben"-Berechtigung in diesem Ordner',
"fp_eperm": 'Einfügen fehlgeschlagen:\nDir fehlt die "Schreiben"-Berechtigung in diesem Ordner',
"fr_emore": "Wähle mindestens ein Element zum Umbenennen aus",
"fd_emore": "Wähle mindestens ein Element zum Löschen aus",
"fc_emore": "Wähle mindestens ein Element zum Ausschneiden aus",
"fcp_emore": "Wähle mindestens ein Element aus, um es in die Zwischenablage zu kopieren",
"fs_sc": "Teile diesen Ordner",
"fs_ss": "Teile die ausgewählten Dateien",
"fs_just1d": "Du kannst nicht mehrere Ordner auswählen \noder Dateien und Ordner in der Auswahl mischen.",
"fs_abrt": "❌ Abbrechen",
"fs_rand": "🎲 Zufallsname",
"fs_go": "✅ Share erstellen",
"fs_name": "Name",
"fs_src": "Quelle",
"fs_pwd": "Passwort",
"fs_exp": "Ablauf",
"fs_tmin": "Minuten",
"fs_thrs": "Stunden",
"fs_tdays": "Tage",
"fs_never": "nie",
"fs_pname": "optionaler Linkname; zufällig wenn leer",
"fs_tsrc": "zu teilende Datei oder Ordner",
"fs_ppwd": "optionales Passwort",
"fs_w8": "erstelle Share...",
"fs_ok": "drücke <code>Eingabe/OK</code> für Zwischenablage\ndrücke <code>ESC/Abbrechen</code> zum Schliessen",
"frt_dec": "Kann Fälle von beschädigten Dateien beheben\">url-decode",
"frt_rst": "Geänderte Dateinamen auf Orginale zurücksetzen\">↺ zurücksetzen",
"frt_abrt": "Abbrechen und dieses Fenster schliessen\">❌ abbrechen",
"frb_apply": "ÜBERNEHMEN",
"fr_adv": "Stapel-/Metadaten-/Musterumbenennung\">erweitert",
"fr_case": "Groß-/Kleinschreibung beachten (Regex)\">Großschreibung",
"fr_win": "Windows-kompatible Namen; ersetzt <code>&lt;&gt;:&quot;\\|?*</code> durch japanische Fullwidth-Zeichen\">win",
"fr_slash": "Ersetzt <code>/</code> durch ein Zeichen, das keine neuen Ordner erstellt\">no /",
"fr_re": "Regex-Suchmuster für Originaldateinamen; Erfassungsgruppen können im Formatfeld unten als &lt;code&gt;(1)&lt;/code&gt; und &lt;code&gt;(2)&lt;/code&gt; usw. referenziert werden",
"fr_fmt": "inspiriert von foobar2000:$N&lt;code&gt;(title)&lt;/code&gt; wird durch Songtitel ersetzt,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; überspringt [diesen] Teil falls Interpret leer$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; füllt die Titelnummer auf 2 Ziffern auf",
"fr_pdel": "Löschen",
"fr_pnew": "Speichern als",
"fr_pname": "Gib der Vorlage einen Namen",
"fr_aborted": "Abgebrochen",
"fr_lold": "Alter Name",
"fr_lnew": "Neuer Name",
"fr_tags": "Tags für die ausgewählten Dateien (liest nur, als Referenz):",
"fr_busy": "Benenne {0} Elemente um...\n\n{1}",
"fr_efail": "Umbenennen fehlgeschlagen:\n",
"fr_nchg": "{0} der neuen Namen wurden angepasst durch <code>win</code> und/oder <code>no /</code>\n\nMöchtest du mit diesen geänderten Namen fortfahren?",
"fd_ok": "Löschen OK",
"fd_err": "Löschen fehlgeschlagen:\n",
"fd_none": "Nichts würde gelöscht; vielleicht durch die Serverkonfiguration blockiert (xbd)?",
"fd_busy": "Lösche {0} Elemente...\n\n{1}",
"fd_warn1": "Diese {0} Elemente LÖSCHEN?",
"fd_warn2": "<b>Ich frage das letzte Mal!</b> Was weg ist, ist weg. Keine Chance, das rückgängig zu machen. Löschen?",
"fc_ok": "{0} Elemente ausgeschnitten",
"fc_warn": '{0} Elemente in die Zwischenablage kopiert\n\nAber: nur <b>dieses</b> Browsertab kann sie einfügen\n(da deine Auswahl so abartig riesig war)',
"fcc_ok": "{0} Elemente in die Zwischenablage kopiert",
"fcc_warn": '{0} Elemente in die Zwischenablage kopiert\n\nAber: nur <b>dieses</b> Browsertab kann sie einfügen\n(da deine Auswahl so abartig riesig war)',
"fp_apply": "Diese Namen verwenden",
"fp_ecut": "Kopiere erst ein paar Dateien / Ordner, um sie einzufügen\n\nTipp: Ausschneiden und Kopieren funktioniert über Browsertabs hinweg",
"fp_ename": "{0} Elemente konnten nicht verschoben werden, weil bereits andere Dateien mit diesen Namen existieren. Gib ihnen unten neue Namen um fortzufahren, oder lass das Feld leer zum Überspringen:",
"fcp_ename": "{0} Elemente konnten nicht kopiert werden, weil bereits andere Dateien mit diesen Namen existieren. Gib ihnen unten neue Namen um fortzufahren, oder lass das Feld leer zum Überspringen:",
"fp_emore": "Es gibt noch ein paar Dateinamen, die geändert werden müssen",
"fp_ok": "Verschieben OK",
"fcp_ok": "Kopieren OK",
"fp_busy": "Verschiebe {0} Elemente...\n\n{1}",
"fcp_busy": "Kopiere {0} Elemente...\n\n{1}",
"fp_err": "Verschieben fehlgeschlagen:\n",
"fcp_err": "Kopieren fehlgeschlagen:\n",
"fp_confirm": "Diese {0} Elemente hierher verschieben?",
"fcp_confirm": "Diese {0} Elemente hierher kopieren?",
"fp_etab": 'Konnte die Zwischenablage nicht vom anderen Browsertab lesen',
"fp_name": "Lade Datei von deinem Gerät hoch. Gib ihr einen Namen:",
"fp_both_m": '<h6>Wähle, was eingefügt werden soll</h6><code>Eingabe</code> = {0} Dateien von «{1}» verschieben\n<code>ESC</code> = {2} Dateien von deinem Gerät hochladen',
"fcp_both_m": '<h6>Wähle, was eingefügt werden soll</h6><code>Eingabe</code> = {0} Dateien von «{1}» kopieren\n<code>ESC</code> = {2} Dateien von deinem Gerät hochladen',
"fp_both_b": '<a href="#" id="modal-ok">Verschieben</a><a href="#" id="modal-ng">Hochladen</a>',
"fcp_both_b": '<a href="#" id="modal-ok">Kopieren</a><a href="#" id="modal-ng">Hochladen</a>',
"mk_noname": "Tipp' mal vorher lieber einen Namen in das Textfeld links, bevor du das machst :p",
"tv_load": "Textdatei wird geladen:\n\n{0}\n\n{1}% ({2} von {3} MiB geladen)",
"tv_xe1": "Konnte Textdatei nicht laden:\n\nFehler ",
"tv_xe2": "404, Datei nicht gefunden",
"tv_lst": "Liste der Textdateien in",
"tvt_close": "Zu Ordneransicht zurück$NHotkey: M (oder Esc)\">❌ Schliessen",
"tvt_dl": "Diese Datei herunterladen$NHotkey: Y\">💾 Herunterladen",
"tvt_prev": "Vorheriges Dokument zeigen$NHotkey: i\">⬆ vorh.",
"tvt_next": "Nächstes Dokument zeigen$NHotkey: K\">⬇ nächst.",
"tvt_sel": "Wählt diese Datei aus &nbsp; ( zum Ausschneiden / Kopieren / Löschen / ... )$NHotkey: S\">ausw.",
"tvt_edit": "Datei im Texteditor zum Bearbeiten öffnen$NHotkey: E\">✏️ bearb.",
"tvt_tail": "Datei auf Veränderungen überwachen; Neue Zeilen werden in Echtzeit angezeigt\">📡 folgen",
"tvt_wrap": "Zeilenumbruch\">↵",
"tvt_atail": "Automatisch nach unten scrollen\">⚓",
"tvt_ctail": "Terminal-Farben dekodieren (ANSI Escape Codes)\">🌈",
"tvt_ntail": "Scrollback limitieren (Menge an Bytes an Text, die geladen bleiben sollen)",
"m3u_add1": "Song wurde zur M3U-Playlist hinzugefügt",
"m3u_addn": "{0} Songs zur M3U-Playlist hinzugefügt",
"m3u_clip": "M3U-Playlist in die Zwischenablage kopiert\n\nDu solltest eine neue Datei mit dem Namen something.m3u erstellen und die Playlist da rein kopieren; damit wird die Playlist abspielbar",
"gt_vau": "nur Ton abspielen, kein Video zeigen\">🎧",
"gt_msel": "Dateiauswahl aktivieren; STRG-klicke eine Datei zum überschreiben$N$N&lt;em&gt;wenn aktiv: Datei / Ordner doppelklicken zum Öffnen&lt;/em&gt;$N$NHotkey: S\">multiselect",
"gt_crop": "Vorschaubilder mittig zuschneiden\">crop",
"gt_3x": "hochauflösende Vorschaubilder\">3x",
"gt_zoom": "zoom",
"gt_chop": "kürzen",
"gt_sort": "sortieren nach",
"gt_name": "Name",
"gt_sz": "Grösse",
"gt_ts": "Datum",
"gt_ext": "Typ",
"gt_c1": "Dateinamen mehr kürzen (weniger zeigen)",
"gt_c2": "Dateinamen weniger kürzen (mehr zeigen)",
"sm_w8": "Suche ...",
"sm_prev": "Die Suchresultate gehören zu einer vorherigen Suchanfrage:\n ",
"sl_close": "Suchresultate schliessen",
"sl_hits": "Zeige {0} Treffer",
"sl_moar": "Mehr laden",
"s_sz": "Grösse",
"s_dt": "Datum",
"s_rd": "Pfad",
"s_fn": "Name",
"s_ta": "Tags",
"s_ua": "up@",
"s_ad": "adv.",
"s_s1": "minimum MiB",
"s_s2": "maximum MiB",
"s_d1": "min. iso8601",
"s_d2": "max. iso8601",
"s_u1": "hochgeladen nach",
"s_u2": "und/oder vor",
"s_r1": "Pfad enthält &nbsp; (Leerzeichen-separiert)",
"s_f1": "Name enthält &nbsp; (negieren mit -nope)",
"s_t1": "Tags enthält &nbsp; (^=start, end=$)",
"s_a1": "spezifische Metadaten-Eigenschaften",
"md_eshow": "Kann nicht rendern ",
"md_off": "[📜<em>readme</em>] deaktiviert in [⚙️] -- Dokument versteckt",
"badreply": "Hab die Antwort vom Server nicht verstanden. (badreply)",
"xhr403": "403: Zugriff verweigert\n\nVersuche, F5 zu drücken. Vielleicht wurdest du abgemeldet.",
"xhr0": "Unbekannt (wahrschenlich Verbindung zum Server verloren oder der Server ist offline)",
"cf_ok": "Sorry dafür -- Der DD" + wah + "oS-Schutz hat angeschlagen.\n\nEs sollte in etwa 30 Sekunden weitergehen.\n\nFalls nichts passiert, drück' F5, um die Seite neuzuladen",
"tl_xe1": "Konnte Unterordner nicht auflisten:\n\nFehler ",
"tl_xe2": "404: Ordner nicht gefunden",
"fl_xe1": "Konnte Dateien in Ordner nicht auflisten:\n\nFehler ",
"fl_xe2": "404: Ordner nicht gefunden",
"fd_xe1": "Konnte Unterordner nicht erstellen:\n\nFehler ",
"fd_xe2": "404: Übergeordneter Ordner nicht gefunden",
"fsm_xe1": "Konnte Nachricht nicht senden:\n\nFehler ",
"fsm_xe2": "404: Übergeordneter Ordner nicht gefunden",
"fu_xe1": "Konnte unpost-Liste nicht laden:\n\nFehler ",
"fu_xe2": "404: Datei nicht gefunden??",
"fz_tar": "Unkomprimierte GNU TAR-Datei (Linux / Mac)",
"fz_pax": "Unkomprimierte pax-format TAR-Datei (etwas langsamer)", //m
"fz_targz": "GNU-TAR mit gzip Level 3 Kompression$N$Nüblicherweise recht langsam,$Nbenutze stattdessen ein unkomprimiertes TAR",
"fz_tarxz": "GNU-TAR mit xz level 1 Kompression$N$Nüblicherweise recht langsam,$Nbenutze stattdessen ein unkomprimiertes TAR",
"fz_zip8": "ZIP mit UTF8-Dateinamen (könnte kaputt gehen auf Windows 7 oder älter)",
"fz_zipd": "ZIP mit traditionellen CP437-Dateinamen, für richtig alte Software",
"fz_zipc": "CP437 mit CRC32 früh berechnet,$Nfür MS-DOS PKZIP v2.04g (Oktober 1993)$N(braucht länger zum Verarbeiten, bevor der Download starten kann)",
"un_m1": "Unten kannst du deine neusten Uploads löschen (oder Unvollständige abbrechen)",
"un_upd": "Neu laden",
"un_m4": "Oder die unten sichtbaren Dateien teilen:",
"un_ulist": "Anzeigen",
"un_ucopy": "Kopieren",
"un_flt": "Optionale Filter:&nbsp; URL muss enthalten",
"un_fclr": "Filter löschen",
"un_derr": 'unpost-delete fehlgeschlagen:\n',
"un_f5": 'Etwas ist kaputt gegangen, versuche die Seite neuzuladen (drücke dazu F5)',
"un_uf5": "Sorry, aber du musst die Seite neuladen (z.B. in dem du F5 oder STRG-R drückst) bevor zu diesen Upload abbrechen kannst",
"un_nou": '<b>Warnung:</b> Der Server ist grade zu beschäftigt, um unvollständige Uploads anzuzeigen; Drücke den "Neu laden"-Link in ein paar Sekunden',
"un_noc": '<b>Warnung:</b> unpost von vollständig hochgeladenen Dateien ist über die Serverkonfiguration gesperrt',
"un_max": "Zeige die ersten 2000 Dateien (benutze Filter, um die gewünschten Dateien zu finden)",
"un_avail": "{0} zuletzt hochgeladene Dateien können gelöscht werden<br />{1} Unvollständige können abgebrochen werden",
"un_m2": "Sortiert nach Upload-Zeitpunkt; neuste zuerst:",
"un_no1": "Hoppala! Es gibt keine ausreichend aktuellen Uploads.",
"un_no2": "Pech gehabt! Kein Upload, der zu dem Filter passen würde, ist neu genug",
"un_next": "Lösche die nächsten {0} Dateien",
"un_abrt": "Abbrechen",
"un_del": "Löschen",
"un_m3": "Deine letzten Uploads werden geladen ...",
"un_busy": "Lösche {0} Dateien ...",
"un_clip": "{0} Links in die Zwischenablage kopiert",
"u_https1": "für bessere Performance solltest du",
"u_https2": "auf HTTPS wechseln",
"u_https3": "",
"u_ancient": 'Dein Browser ist verdammt antik -- vielleicht solltest du <a href="#" onclick="goto(\'bup\')">stattdessen bup benutzen</a>',
"u_nowork": "Benötigt Firefox 53+ oder Chrome 57+ oder iOS 11+",
"tail_2old": "Benötigt Firefox 105+ oder Chrome 71+ oder iOS 14.5+",
"u_nodrop": 'Dein Browser ist zu alt für Drag-and-Drop Uploads',
"u_notdir": "Das ist kein Ordner!\n\nDein Browser ist zu alt,\nversuch stattdessen dragdrop",
"u_uri": "Um Bilder per Drag-and-Drop aus anderen Browserfenstern hochzuladen,\nlass' sie bitte über dem grossen Upload-Button fallen",
"u_enpot": 'Zu <a href="#">Potato UI</a> wechseln (kann Upload-Geschw. verbessern)',
"u_depot": 'Zu <a href="#">fancy UI</a> wechseln (kann Upload-Geschw. verschlechtern)',
"u_gotpot": 'Wechsle zu Potato UI für verbesserte Upload-Geschwindigkeit,\n\nwenn du anderer Meinung bist, kannst du gerne zurück wechseln',
"u_pott": "<p>Dateien: &nbsp; <b>{0}</b> fertig, &nbsp; <b>{1}</b> fehlgeschlagen, &nbsp; <b>{2}</b> in Bearbeitung, &nbsp; <b>{3}</b> ausstehend</p>",
"u_ever": "Dies ist der Basic Uploader; up2k benötigt mind. <br>Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1",
"u_su2k": 'Dies ist der Basic Uploader; <a href="#" id="u2yea">up2k</a> ist besser',
"u_uput": 'Für Geschwindigkeit optimieren (Checksum überspringen)',
"u_ewrite": 'Du hast kein Schreibzugriff auf diesen Ordner',
"u_eread": 'Du hast kein Lesezugriff auf diesen Ordner',
"u_enoi": 'file-search ist in der Serverkonfiguration nicht aktiviert',
"u_enoow": "Überschreiben wird hier nicht funktionieren; benötige Lösch-Berechtigung",
"u_badf": 'Diese {0} Dateien (von insgesammt {1}) wurden übersprungen, wahrscheinlich wegen Dateisystem-Berechtigungen:\n\n',
"u_blankf": 'Diese {0} Dateien (von insgesammt {1}) sind leer; trotzdem hochladen?\n\n',
"u_applef": 'Diese {0} Dateien (von insgesammt {1}) sind möglicherweise unerwünscht;\n<code>OK/Eingabe</code> drücken, um die folgenden Dateien zu überspringen.\nDrücke <code>Abbrechen/ESC</code> um sie NICHT zu überspringen und diese AUCH HOCHZULADEN:\n\n',
"u_just1": '\nFunktioniert vielleicht besser, wenn du nur eine Datei auswählst',
"u_ff_many": "Falls du <b>Linux / MacOS / Android</b> benutzt, <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1790500\" target=\"_blank\"><em>könnte</em> Firefox mit dieser Menge an Dateien crashen!</a>\nFalls das passiert, probier nochmal (oder benutz Chrome).",
"u_up_life": "Dieser Upload wird vom Server gelöscht\n{0} nachdem er abgeschlossen ist",
"u_asku": 'Diese {0} Dateien nach <code>{1}</code> hochladen',
"u_unpt": "Du kannst diesen Upload rückgängig machen mit dem 🧯 oben-links",
"u_bigtab": 'Versuche {0} Dateien anzuzeigen.\n\nDas könnte dein Browser crashen, bist du dir wirklich sicher?',
"u_scan": 'Scanne Dateien...',
"u_dirstuck": 'Ordner-Iterator blieb hängen beim Versuch, diese {0} Einträge zu lesen; überspringe:',
"u_etadone": 'Fertig ({0}, {1} Dateien)',
"u_etaprep": '(Upload wird vorbereitet)',
"u_hashdone": 'Hashing vollständig',
"u_hashing": 'Hash',
"u_hs": 'Wir schütteln uns die Hände ("handshaking")...',
"u_started": "Dateien werden hochgeladen; siehe [🚀]",
"u_dupdefer": "Duplikat; wird nach allen anderen Dateien verarbeitet",
"u_actx": "Klicke diesen Text um Performance-<br />Einbusen zu Vermeiden beim Wechsel auf andere Fenster/Tabs",
"u_fixed": "OK!&nbsp; Habs repariert 👍",
"u_cuerr": "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}",
"u_cuerr2": "server rejected upload (chunk {0} of {1});\nwill retry later\n\nfile: {2}\n\nerror ",
"u_ehstmp": "versuche nochmal; siehe unten-rechts",
"u_ehsfin": "Der Server hat die Anfrage zum Abschluss des Uploads abgelehnt; versuche nochmal...",
"u_ehssrch": "Der Server hat die Anfrage zur Suche abgelehnt; versuche nochmal...",
"u_ehsinit": "Der Server hat die Anfrage zum Start des Uploads abgelehnt; versuche nochmal...",
"u_eneths": "Netzwerkfehler beim Upload-Handshake; versuche nochmal...",
"u_enethd": "Netzwerkfehler beim Testen der Existenz des Ziels; versuche nochmal...",
"u_cbusy": "Der Server mag uns grade nicht mehr nach einem Netzwerkglitch, warte einen Moment...",
"u_ehsdf": "Server hat kein Speicherplatz mehr!\n\nwerde es erneut versuchen, falls jemand\ngenug Platz schafft um fortzufahren",
"u_emtleak1": "scheint, als ob dein Browser ein Memory Leak hätte;\nbitte",
"u_emtleak2": ' <a href="{0}">wechsle auf HTTPS (empfohlen)</a> oder ',
"u_emtleak3": '',
"u_emtleakc": 'versuche folgendes:\n<ul><li>drücke <code>F5</code> um die Seite neu zu laden</li><li>deaktivere dann den &nbsp;<code>mt</code>&nbsp; Button in den &nbsp;<code>⚙️ Einstellungen</code></li><li>und versuche den Upload nochmal.</li></ul>Uploads werden etwas langsamer sein, aber man kann ja nicht alles haben.\nSorry für die Umstände !\n\nPS: Chrome v107 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1354816" target="_blank">hat ein Bugfix</a> dafür',
"u_emtleakf": 'versuche folgendes:\n<ul><li>drücke <code>F5</code> um die Seite neu zu laden</li><li>aktivere dann <code>🥔</code> (potato) im Upload UI<li>und versuche den Upload nochmal</li></ul>\nPS: Firefox <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1790500" target="_blank">hat hoffentlich irgendwann ein Bugfix</a>',
"u_s404": "nicht auf dem Server gefunden",
"u_expl": "erklären",
"u_maxconn": "die meisten Browser limitieren dies auf 6, aber Firefox lässt mehr zu unter <code>connections-per-server</code> in <code>about:config</code>",
"u_tu": '<p class="warn">WARNUNG: Turbo aktiviert, <span>&nbsp;Client könnte unvollständige Uploads verpassen und nicht wiederholen; siehe Turbo-Button Tooltip</span></p>',
"u_ts": '<p class="warn">WARNUNG: Turbo aktiviert, <span>&nbsp;Suchresultate können inkorrekt sein; siehe Turbo-Button Tooltip</span></p>',
"u_turbo_c": "Turbo deaktiviert in der Serverkonfiguration",
"u_turbo_g": "Turbo deaktiviert, da du keine Listen-Berechtigung\nauf diesem Volume hast",
"u_life_cfg": 'Autodelete nach <input id="lifem" p="60" /> min (or <input id="lifeh" p="3600" /> h)',
"u_life_est": 'Upload wird gelöscht <span id="lifew" tt="local time">---</span>',
"u_life_max": 'Dieser Ordner erzwingt eine\nmax Lebensdauer von {0}',
"u_unp_ok": 'unpost ist erlaubt für {0}',
"u_unp_ng": 'unpost wird NICHT erlaubt',
"ue_ro": 'Du hast nur Lese-Zugriff auf diesen Ordner\n\n',
"ue_nl": 'Du bist nicht angemeldet',
"ue_la": 'Du bist angemeldet als "{0}"',
"ue_sr": 'Du bist derzeit im Suchmodus\n\nWechsle zum Upload-Modus indem du auf die Lupe 🔎 klickst (neben dem grossen SUCHEN Button), und versuche den Upload nochmal.\n\nSorry',
"ue_ta": 'Versuche den Upload nochmal, sollte jetzt klappen',
"ue_ab": "Diese Datei wird gerade in einem anderen Ordner hochgeladen, dieser Upload muss zuerst abgeschlossen werden, bevor die Datei woanders hochgeladen werden kann.\n\nDu kannst den Upload abbrechen und vergessen mit dem 🧯 oben-links",
"ur_1uo": "OK: Datei erfolgreich hochgeladen",
"ur_auo": "OK: Alle {0} Dateien erfolgreich hochgeladen",
"ur_1so": "OK: Datei auf dem Server gefunden",
"ur_aso": "OK: Alle {0} Dateien auf dem Server gefunden",
"ur_1un": "Upload fehlgeschlagen, sorry",
"ur_aun": "Alle {0} Uploads fehlgeschlagen, sorry",
"ur_1sn": "Datei wurde NICHT auf dem Server gefunden",
"ur_asn": "Die {0} Dateien wurden NICHT auf dem Server gefunden",
"ur_um": "Fertig;\n{0} Uploads OK,\n{1} Uploads fehlgeschlagen, sorry",
"ur_sm": "Fertig;\n{0} Uploads gefunden auf dem Server,\n{1} Dateien NICHT gefunden auf dem Server",
"lang_set": "Neuladen um Änderungen anzuwenden?",
},
}; };
var LANGS = ["eng", "nor", "chi"]; var LANGS = ["eng", "nor", "chi", "deu"];
if (window.langmod) if (window.langmod)
langmod(); langmod();
@@ -2090,6 +2725,7 @@ ebi('op_cfg').innerHTML = (
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '</a>\n' + ' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '</a>\n' +
' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '</a>\n' + ' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '</a>\n' +
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '</a>\n' + ' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '</a>\n' +
' <a id="qdel" class="tgl btn" href="#" tt="' + L.ct_qdel + '</a>\n' +
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '</a>\n' + ' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '</a>\n' +
' <a id="nsort" class="tgl btn" href="#" tt="' + L.ct_nsort + '</a>\n' + ' <a id="nsort" class="tgl btn" href="#" tt="' + L.ct_nsort + '</a>\n' +
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '</a>\n' + ' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '</a>\n' +
@@ -2362,6 +2998,7 @@ var mpl = (function () {
ebi('op_player').innerHTML = ( ebi('op_player').innerHTML = (
'<div><h3>' + L.cl_opts + '</h3><div>' + '<div><h3>' + L.cl_opts + '</h3><div>' +
'<a href="#" class="tgl btn" id="au_loop" tt="' + L.mt_loop + '</a>' + '<a href="#" class="tgl btn" id="au_loop" tt="' + L.mt_loop + '</a>' +
'<a href="#" class="tgl btn" id="au_one" tt="' + L.mt_one + '</a>' +
'<a href="#" class="tgl btn" id="au_shuf" tt="' + L.mt_shuf + '</a>' + '<a href="#" class="tgl btn" id="au_shuf" tt="' + L.mt_shuf + '</a>' +
'<a href="#" class="tgl btn" id="au_aplay" tt="' + L.mt_aplay + '</a>' + '<a href="#" class="tgl btn" id="au_aplay" tt="' + L.mt_aplay + '</a>' +
'<a href="#" class="tgl btn" id="au_preload" tt="' + L.mt_preload + '</a>' + '<a href="#" class="tgl btn" id="au_preload" tt="' + L.mt_preload + '</a>' +
@@ -2385,6 +3022,7 @@ var mpl = (function () {
'<div><h3>' + L.ml_pmode + '</h3><div id="pb_mode">' + '<div><h3>' + L.ml_pmode + '</h3><div id="pb_mode">' +
'<a href="#" class="tgl btn" m="loop" tt="' + L.mt_mloop + '</a>' + '<a href="#" class="tgl btn" m="loop" tt="' + L.mt_mloop + '</a>' +
'<a href="#" class="tgl btn" m="next" tt="' + L.mt_mnext + '</a>' + '<a href="#" class="tgl btn" m="next" tt="' + L.mt_mnext + '</a>' +
'<a href="#" class="tgl btn" m="stop" tt="' + L.mt_mstop + '</a>' +
'</div></div>' + '</div></div>' +
(have_acode ? ( (have_acode ? (
@@ -2410,11 +3048,15 @@ var mpl = (function () {
''); '');
var r = { var r = {
"pb_mode": (sread('pb_mode', ['loop', 'next']) || 'next').split('-')[0], "pb_mode": (sread('pb_mode', ['loop', 'next', 'stop']) || 'next').split('-')[0],
"os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl, "os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl,
'traversals': 0, 'traversals': 0,
'm3ut': '#EXTM3U\n', 'm3ut': '#EXTM3U\n',
}; };
bcfg_bind(r, 'one', 'au_one', false, function (v) {
if (mp.au)
mp.au.loop = !v && r.loop;
});
bcfg_bind(r, 'loop', 'au_loop', false, function (v) { bcfg_bind(r, 'loop', 'au_loop', false, function (v) {
if (mp.au) if (mp.au)
mp.au.loop = v; mp.au.loop = v;
@@ -3664,7 +4306,7 @@ var mpui = (function () {
} }
// preload next song // preload next song
if (mpl.preload && preloaded != mp.au.rsrc) { if (!mpl.one && mpl.preload && preloaded != mp.au.rsrc) {
var len = mp.au.duration, var len = mp.au.duration,
rem = pos > 1 ? len - pos : 999, rem = pos > 1 ? len - pos : 999,
full = null; full = null;
@@ -3683,7 +4325,12 @@ var mpui = (function () {
var oi = mp.order.indexOf(mp.au.tid) + 1, var oi = mp.order.indexOf(mp.au.tid) + 1,
evp = get_evpath(); evp = get_evpath();
if (mpl.pb_mode == 'loop' || mp.au.evp != evp || ebi('unsearch')) if (oi >= mp.order.length && (
mpl.one ||
mpl.pb_mode != 'next' ||
mp.au.evp != evp ||
ebi('unsearch'))
)
oi = 0; oi = 0;
if (oi >= mp.order.length) { if (oi >= mp.order.length) {
@@ -4169,6 +4816,9 @@ function play(tid, is_ev, seek) {
} }
if (tn >= mp.order.length) { if (tn >= mp.order.length) {
if (mpl.pb_mode == 'stop')
return;
if (mpl.pb_mode == 'loop' || ebi('unsearch')) { if (mpl.pb_mode == 'loop' || ebi('unsearch')) {
tn = 0; tn = 0;
} }
@@ -4253,7 +4903,7 @@ function play(tid, is_ev, seek) {
try { try {
mp.nopause(); mp.nopause();
mp.au.loop = mpl.loop; mp.au.loop = mpl.loop && !mpl.one;
if (mpl.aplay || is_ev !== -1) if (mpl.aplay || is_ev !== -1)
mp.au.play(); mp.au.play();
@@ -4299,6 +4949,8 @@ function scroll2playing() {
function evau_end(e) { function evau_end(e) {
if (mpl.one)
return;
if (!mpl.loop) if (!mpl.loop)
return next_song(e); return next_song(e);
ev(e); ev(e);
@@ -5453,7 +6105,16 @@ var fileman = (function () {
deleter(); deleter();
} }
var asks = r.qdel ? 1 : 2;
if (dqdel === 0)
asks -= 1;
if (!asks)
return deleter();
modal.confirm('<h6 style="color:#900">' + L.danger + '</h6>\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () { modal.confirm('<h6 style="color:#900">' + L.danger + '</h6>\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {
if (asks === 1)
return deleter();
modal.confirm(L.fd_warn2, deleter, null); modal.confirm(L.fd_warn2, deleter, null);
}, null); }, null);
}; };
@@ -5814,6 +6475,8 @@ var fileman = (function () {
r.bus.onmessage(); r.bus.onmessage();
}; };
bcfg_bind(r, 'qdel', 'qdel', dqdel == 1);
bren.onclick = r.rename; bren.onclick = r.rename;
bdel.onclick = r.delete; bdel.onclick = r.delete;
bcut.onclick = r.cut; bcut.onclick = r.cut;
@@ -6009,6 +6672,7 @@ var showfile = (function () {
m = /[?&](k=[^&#]+)/.exec(url); m = /[?&](k=[^&#]+)/.exec(url);
url = url.split('?')[0] + (m ? '?' + m[1] : ''); url = url.split('?')[0] + (m ? '?' + m[1] : '');
assert_vp(url);
if (r.taildoc) if (r.taildoc)
return r.tail(url, no_push); return r.tail(url, no_push);
@@ -7427,7 +8091,7 @@ var search_ui = (function () {
nodes = ['<tr><td>-</td><td><div>' + links + '</div>', sz]; nodes = ['<tr><td>-</td><td><div>' + links + '</div>', sz];
for (var b = 0; b < tagord.length; b++) { for (var b = 0; b < tagord.length; b++) {
var k = tagord[b], var k = esc(tagord[b]),
v = r.tags[k] || ""; v = r.tags[k] || "";
if (k == ".dur") { if (k == ".dur") {
@@ -7436,7 +8100,7 @@ var search_ui = (function () {
continue; continue;
} }
nodes.push(v); nodes.push(esc('' + v));
} }
nodes = nodes.concat([ext, unix2iso(ts)]); nodes = nodes.concat([ext, unix2iso(ts)]);
@@ -7503,6 +8167,7 @@ function ev_load_m3u(e) {
return false; return false;
} }
function load_m3u(url) { function load_m3u(url) {
assert_vp(url);
var xhr = new XHR(); var xhr = new XHR();
xhr.open('GET', url, true); xhr.open('GET', url, true);
xhr.onload = render_m3u; xhr.onload = render_m3u;
@@ -7907,6 +8572,17 @@ var treectl = (function () {
return toast.err(30, "bad <code>?tree</code> reply;\nexpected json, got this:\n\n" + esc(this.responseText + '')); return toast.err(30, "bad <code>?tree</code> reply;\nexpected json, got this:\n\n" + esc(this.responseText + ''));
} }
r.rendertree(res, this.ts, this.top, this.dst, this.rst); r.rendertree(res, this.ts, this.top, this.dst, this.rst);
if (r.lsc && r.lsc.unlist)
r.prunetree(r.lsc);
};
r.prunetree = function (res) {
var ptn = new RegExp(res.unlist);
var els = QSA('#treeul li>a+a');
for (var a = els.length - 1; a >= 0; a--)
if (ptn.exec(els[a].textContent) && !els[a].className)
els[a].closest('ul').removeChild(els[a].closest('li'));
}; };
r.rendertree = function (res, ts, top0, dst, rst) { r.rendertree = function (res, ts, top0, dst, rst) {
@@ -8194,6 +8870,8 @@ var treectl = (function () {
} }
r.rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : '')); r.rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : ''));
if (res.unlist)
r.prunetree(res);
} }
r.gentab(this.top, res); r.gentab(this.top, res);
@@ -8254,6 +8932,7 @@ var treectl = (function () {
}; };
r.gentab = function (top, res) { r.gentab = function (top, res) {
showfile.untail();
var nodes = res.dirs.concat(res.files), var nodes = res.dirs.concat(res.files),
html = mk_files_header(res.taglist), html = mk_files_header(res.taglist),
sel = msel.hist[top], sel = msel.hist[top],
@@ -8274,7 +8953,7 @@ var treectl = (function () {
if (res.unlist) { if (res.unlist) {
var ptn = new RegExp(res.unlist); var ptn = new RegExp(res.unlist);
for (var a = nodes.length - 1; a >= 0; a--) for (var a = nodes.length - 1; a >= 0; a--)
if (ptn.exec(nodes[a].href.split('?')[0])) if (ptn.exec(uricom_dec(nodes[a].href.split('?')[0])))
nodes.splice(a, 1); nodes.splice(a, 1);
} }
nodes = sortfiles(nodes); nodes = sortfiles(nodes);
@@ -8324,7 +9003,7 @@ var treectl = (function () {
top + tn.href + '" id="' + id + '">' + hname + '</a>', tn.sz]; top + tn.href + '" id="' + id + '">' + hname + '</a>', tn.sz];
for (var b = 0; b < res.taglist.length; b++) { for (var b = 0; b < res.taglist.length; b++) {
var k = res.taglist[b], var k = esc(res.taglist[b]),
v = (tn.tags || {})[k] || "", v = (tn.tags || {})[k] || "",
sv = null; sv = null;
@@ -8333,7 +9012,7 @@ var treectl = (function () {
else if (k == ".up_at") else if (k == ".up_at")
sv = v ? unix2iso(v) : ""; sv = v ? unix2iso(v) : "";
else { else {
ln.push(v); ln.push(esc('' + v));
continue; continue;
} }
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv; ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;

View File

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

View File

@@ -92,7 +92,49 @@ var Ls = {
"ae1": "正在下载:", //m "ae1": "正在下载:", //m
"af1": "显示最近上传的文件", //m "af1": "显示最近上传的文件", //m
"ag1": "查看已知 IdP 用户", //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) if (window.langmod)

View File

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

View File

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

View File

@@ -383,8 +383,10 @@ if (!String.prototype.format)
}); });
}; };
var have_URL = false;
try { try {
new URL('/a/', 'https://a.com/'); new URL('/a/', 'https://a.com/');
have_URL = true;
} }
catch (ex) { catch (ex) {
console.log('ie11 shim URL()'); console.log('ie11 shim URL()');
@@ -732,6 +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) { function linksplit(rp, id) {
var ret = [], var ret = [],
apath = '/', apath = '/',

View File

@@ -1,3 +1,78 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 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 # 2025-0707-1419 `v1.18.2` idp-vol persistence

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,16 +4,30 @@
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils }: outputs =
{
self,
nixpkgs,
flake-utils,
}:
{ {
nixosModules.default = ./contrib/nixos/modules/copyparty.nix; nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
overlays.default = self: super: { overlays.default = final: prev: rec {
copyparty = copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
self.python3.pkgs.callPackage ./contrib/package/nix/copyparty { ffmpeg = final.ffmpeg-full;
ffmpeg = self.ffmpeg-full; };
};
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 let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
@@ -22,10 +36,31 @@
}; };
overlays = [ self.overlays.default ]; overlays = [ self.overlays.default ];
}; };
in { in
{
# check that copyparty builds with all optionals turned on
checks.copyparty-full = self.packages.${system}.copyparty.override {
withHashedPasswords = true;
withCertgen = true;
withThumbnails = true;
withFastThumbnails = true;
withMediaProcessing = true;
withBasicAudioMetadata = true;
withZeroMQ = true;
withFTPS = true;
withSMB = true;
};
packages = { packages = {
inherit (pkgs) copyparty; inherit (pkgs)
copyparty
partyfuse
u2c
;
default = self.packages.${system}.copyparty; default = self.packages.${system}.copyparty;
}; };
});
formatter = pkgs.nixfmt-tree;
}
);
} }

View File

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

View File

@@ -12,7 +12,7 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/ COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \ RUN apk add -U !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \ py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
py3-pip py3-cffi \ py3-pip py3-cffi \
ffmpeg \ ffmpeg \
py3-magic \ py3-magic \

View File

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

View File

@@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
RUN apk add -U !pyc \ RUN apk add -U !pyc \
tzdata wget mimalloc2 mimalloc2-insecure \ tzdata wget mimalloc2 mimalloc2-insecure \
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \ py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
py3-pip py3-cffi \ py3-pip py3-cffi \
ffmpeg \ ffmpeg \
py3-magic \ py3-magic \

View File

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

View File

@@ -121,7 +121,7 @@ var tl_browser = {
"file-manager", "file-manager",
["G", "toggle list / grid view"], ["G", "toggle list / grid view"],
["T", "toggle thumbnails / icons"], ["T", "toggle thumbnails / icons"],
["🡅 A/D", "thumbnail size"], [" A/D", "thumbnail size"],
["ctrl-K", "delete selected"], ["ctrl-K", "delete selected"],
["ctrl-X", "cut selection to clipboard"], ["ctrl-X", "cut selection to clipboard"],
["ctrl-C", "copy selection to clipboard"], ["ctrl-C", "copy selection to clipboard"],
@@ -131,9 +131,9 @@ var tl_browser = {
"file-list-sel", "file-list-sel",
["space", "toggle file selection"], ["space", "toggle file selection"],
["🡑/🡓", "move selection cursor"], ["↑/↓", "move selection cursor"],
["ctrl 🡑/🡓", "move cursor and viewport"], ["ctrl ↑/↓", "move cursor and viewport"],
["🡅 🡑/🡓", "select prev/next file"], ["⇧ ↑/↓", "select prev/next file"],
["ctrl-A", "select all files / folders"], ["ctrl-A", "select all files / folders"],
], [ ], [
"navigation", "navigation",
@@ -156,7 +156,7 @@ var tl_browser = {
["Home/End", "first/last pic"], ["Home/End", "first/last pic"],
["F", "fullscreen"], ["F", "fullscreen"],
["R", "rotate clockwise"], ["R", "rotate clockwise"],
["🡅 R", "rotate ccw"], [" R", "rotate ccw"],
["S", "select pic"], ["S", "select pic"],
["Y", "download pic"], ["Y", "download pic"],
], [ ], [
@@ -312,6 +312,7 @@ var tl_browser = {
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', "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_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_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_dir1st": 'sort folders before files">📁 first',
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort', "ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
"ct_readme": 'show README.md in folder listings">📜 readme', "ct_readme": 'show README.md in folder listings">📜 readme',
@@ -363,6 +364,7 @@ var tl_browser = {
"ml_drc": "dynamic range compressor", "ml_drc": "dynamic range compressor",
"mt_loop": "loop/repeat one song\">🔁", "mt_loop": "loop/repeat one song\">🔁",
"mt_one": "stop after one song\">1⃣",
"mt_shuf": "shuffle the songs in each folder\">🔀", "mt_shuf": "shuffle the songs in each folder\">🔀",
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶", "mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
"mt_preload": "start loading the next song near the end for gapless playback\">preload", "mt_preload": "start loading the next song near the end for gapless playback\">preload",
@@ -380,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_uncache": "clear cache &nbsp;(try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
"mt_mloop": "loop the open folder\">🔁 loop", "mt_mloop": "loop the open folder\">🔁 loop",
"mt_mnext": "load the next folder and continue\">📂 next", "mt_mnext": "load the next folder and continue\">📂 next",
"mt_mstop": "stop playback\">⏸ stop",
"mt_cflac": "convert flac / wav to opus\">flac", "mt_cflac": "convert flac / wav to opus\">flac",
"mt_caac": "convert aac / m4a to opus\">aac", "mt_caac": "convert aac / m4a to opus\">aac",
"mt_coth": "convert all others (not mp3) to opus\">oth", "mt_coth": "convert all others (not mp3) to opus\">oth",

View File

@@ -143,7 +143,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "chpw daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz rmagic rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs" ex = "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()}) ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip" ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip"
@@ -152,7 +152,10 @@ class Cfg(Namespace):
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip" ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
ka.update(**{k: None for k in ex.split()}) ka.update(**{k: None for k in ex.split()})
ex = "hash_mt hsortn safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz" ex = "gid uid"
ka.update(**{k: -1 for k in ex.split()})
ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
ka.update(**{k: 1 for k in ex.split()}) ka.update(**{k: 1 for k in ex.split()})
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who" ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
@@ -164,7 +167,7 @@ class Cfg(Namespace):
ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR" ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico 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()}) ka.update(**{k: "" for k in ex.split()})
ex = "ban_403 ban_404 ban_422 ban_pw ban_url spinner" ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner"
ka.update(**{k: "no" for k in ex.split()}) ka.update(**{k: "no" for k in ex.split()})
ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm" ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"