From 3cde1f3be205f4a6fe333191d19910678c78f866 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 29 Jul 2025 17:13:34 +0000 Subject: [PATCH 01/19] docker-compose: PYTHONUNBUFFERED=1 almost zero performance impact with podman in kitty --- docs/examples/docker/basic-docker-compose/docker-compose.yml | 5 ++++- docs/examples/docker/idp-authelia-traefik/docker-compose.yml | 3 +++ .../examples/docker/idp-authentik-traefik/docker-compose.yml | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/examples/docker/basic-docker-compose/docker-compose.yml b/docs/examples/docker/basic-docker-compose/docker-compose.yml index cfac59b6..dd3c1f9a 100644 --- a/docs/examples/docker/basic-docker-compose/docker-compose.yml +++ b/docs/examples/docker/basic-docker-compose/docker-compose.yml @@ -10,9 +10,12 @@ services: - ./:/cfg:z - /path/to/your/fileshare/top/folder:/w:z - # enabling mimalloc by replacing "NOPE" with "2" will make some stuff twice as fast, but everything will use twice as much ram: environment: LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE + # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) + + PYTHONUNBUFFERED: 1 + # ensures log-messages are not delayed (but can reduce speed a tiny bit) stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal healthcheck: diff --git a/docs/examples/docker/idp-authelia-traefik/docker-compose.yml b/docs/examples/docker/idp-authelia-traefik/docker-compose.yml index 5fe25a15..9ebd73ba 100644 --- a/docs/examples/docker/idp-authelia-traefik/docker-compose.yml +++ b/docs/examples/docker/idp-authelia-traefik/docker-compose.yml @@ -27,6 +27,9 @@ services: LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) + PYTHONUNBUFFERED: 1 + # ensures log-messages are not delayed (but can reduce speed a tiny bit) + authelia: image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax container_name: idp_authelia diff --git a/docs/examples/docker/idp-authentik-traefik/docker-compose.yml b/docs/examples/docker/idp-authentik-traefik/docker-compose.yml index ee10f0f9..7ddf1a9d 100644 --- a/docs/examples/docker/idp-authentik-traefik/docker-compose.yml +++ b/docs/examples/docker/idp-authentik-traefik/docker-compose.yml @@ -27,6 +27,9 @@ services: LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) + PYTHONUNBUFFERED: 1 + # ensures log-messages are not delayed (but can reduce speed a tiny bit) + traefik: image: traefik:v2.11 container_name: traefik From fbf17be203c9d260996de7d6bd3d196554e89ab7 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 29 Jul 2025 18:14:51 +0000 Subject: [PATCH 02/19] apply unlist to navpane too --- copyparty/web/browser.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index ce204757..f1368912 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -7946,6 +7946,17 @@ var treectl = (function () { return toast.err(30, "bad ?tree reply;\nexpected json, got this:\n\n" + esc(this.responseText + '')); } 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) { @@ -8233,6 +8244,8 @@ var treectl = (function () { } r.rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : '')); + if (res.unlist) + r.prunetree(res); } r.gentab(this.top, res); @@ -8314,7 +8327,7 @@ var treectl = (function () { if (res.unlist) { var ptn = new RegExp(res.unlist); 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 = sortfiles(nodes); From 5c6341e99fb11b7bf272beed5d2a56ce0639be65 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 29 Jul 2025 20:03:42 +0000 Subject: [PATCH 03/19] disk-info: both free+total on windows too (#272) --- copyparty/httpcli.py | 8 ++++---- copyparty/util.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 2976a7c1..9052acc0 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -6157,13 +6157,13 @@ class HttpCli(object): self.log("#wow #whoa") if not self.args.nid: - free, total, _ = get_df(abspath, False) - if total is not None: + free, total, zs = get_df(abspath, False) + if total: h1 = humansize(free or 0) h2 = humansize(total) srv_info.append("{} free of {}".format(h1, h2)) - elif free is not None: - srv_info.append(humansize(free, True) + " free") + elif zs: + self.log("diskfree(%r): %s" % (abspath, zs), 3) srv_infot = " // ".join(srv_info) diff --git a/copyparty/util.py b/copyparty/util.py index 0b2dbcc4..86cf046b 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2662,7 +2662,7 @@ def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool: return _fs_mvrm(log, abspath, "", False, flags) -def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str]: +def get_df(abspath: str, prune: bool) -> tuple[int, int, str]: try: ap = fsenc(abspath) while prune and not os.path.isdir(ap) and BOS_SEP in ap: @@ -2673,17 +2673,22 @@ def get_df(abspath: str, prune: bool) -> tuple[Optional[int], Optional[int], str assert ctypes # type: ignore # !rm abspath = fsdec(ap) bfree = ctypes.c_ulonglong(0) + btotal = ctypes.c_ulonglong(0) + bavail = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore - ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree) + ctypes.c_wchar_p(abspath), + ctypes.pointer(bavail), + ctypes.pointer(btotal), + ctypes.pointer(bfree), ) - return (bfree.value, None, "") + return (bavail.value, btotal.value, "") else: sv = os.statvfs(ap) free = sv.f_frsize * sv.f_bfree total = sv.f_frsize * sv.f_blocks return (free, total, "") except Exception as ex: - return (None, None, repr(ex)) + return (0, 0, repr(ex)) if not ANYWIN and not MACOS: From 4988a55ea58f1db7eff0dfbf2d5e53d783eb08f2 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 29 Jul 2025 20:07:11 +0000 Subject: [PATCH 04/19] webdav: send diskfree; closes #272 --- copyparty/httpcli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 9052acc0..8bb12870 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1575,6 +1575,18 @@ class HttpCli(object): self.log("inaccessible: %r" % ("/" + self.vpath,)) raise Pebkac(401, "authenticate") + if "quota-available-bytes" in props and not self.args.nid: + bfree, btot, _ = get_df(vn.realpath, False) + if btot: + df = { + "quota-available-bytes": str(bfree), + "quota-used-bytes": str(btot - bfree), + } + else: + df = {} + else: + df = {} + fgen = itertools.chain([topdir], fgen) vtop = vjoin(self.args.R, vjoin(vn.vpath, rem)) @@ -1617,6 +1629,9 @@ class HttpCli(object): ap = os.path.join(tap, x["vp"]) pvs["getcontenttype"] = html_escape(guess_mime(rp, ap)) pvs["getcontentlength"] = str(st.st_size) + elif df: + pvs.update(df) + df = {} for k, v in pvs.items(): if k not in props: From c3cc2ddeaea4d2c762bce0335b6637ce341861cd Mon Sep 17 00:00:00 2001 From: Jo <141064017+Arklaum@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:24:17 +0100 Subject: [PATCH 05/19] diskfree without root-reserved space (#285) Signed-off-by: Jo <141064017+Arklaum@users.noreply.github.com> --- copyparty/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copyparty/util.py b/copyparty/util.py index 86cf046b..14768e6e 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2684,7 +2684,7 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]: return (bavail.value, btotal.value, "") else: sv = os.statvfs(ap) - free = sv.f_frsize * sv.f_bfree + free = sv.f_frsize * sv.f_bavail total = sv.f_frsize * sv.f_blocks return (free, total, "") except Exception as ex: From 053de61907fa3d83a9c6b0dd699c8e2b973dd3ae Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 17:26:58 +0000 Subject: [PATCH 06/19] 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" --- copyparty/authsrv.py | 19 +++++++++++++++++++ copyparty/httpcli.py | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index bdc65640..03dea807 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -70,6 +70,25 @@ if PY2: LEELOO_DALLAS = "leeloo_dallas" +## +## you might be curious what Leeloo Dallas is doing here, so let me explain: +## +## certain daemonic tasks, namely: +## * deletion of expired files, running on a timer +## * deletion of sidecar files, initiated by plugins +## need to skip the usual permission-checks to do their thing, +## so we let Leeloo handle these +## +## and also, the smb-server has really shitty support for user-accounts +## so one popular way to avoid issues is by running copyparty without users; +## this makes all smb-clients identify as LD to gain unrestricted access +## +## Leeloo, being a fictional character from The Fifth Element, +## obviously does not exist and will never be able to access any copyparty +## instances from the outside (the username is rejected at every entrypoint) +## +## thanks for coming to my ted talk + SEE_LOG = "see log for details" SEESLOG = " (see serverlog for details)" diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8bb12870..f90f4d93 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -33,7 +33,7 @@ except: from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode from .__version__ import S_VERSION -from .authsrv import VFS # typechk +from .authsrv import LEELOO_DALLAS, VFS # typechk from .bos import bos from .star import StreamTar from .stolen.qrcodegen import QrCode, qr2svg @@ -622,6 +622,9 @@ class HttpCli(object): ) or self.args.idp_h_key in self.headers if trusted_key and trusted_xff: + if idp_usr.lower() == LEELOO_DALLAS: + self.loud_reply("send her back", status=403) + return False self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp) else: if not trusted_key: From a9d07c63ed82b11c172009a767c6dbb4f1f78d03 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 18:02:11 +0000 Subject: [PATCH 07/19] disable libmagic on windows; probably closes #276 --- README.md | 1 + copyparty/util.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05661ed4..8dad60c6 100644 --- a/README.md +++ b/README.md @@ -2226,6 +2226,7 @@ force-enable features with known issues on your OS/env by setting any of the fo | env-var | what it does | | ------------------------ | ------------ | | `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms | +| `PRTY_FORCE_MAGIC` | use [magic](https://pypi.org/project/python-magic/) on Windows (you will segfault) | # packages diff --git a/copyparty/util.py b/copyparty/util.py index 14768e6e..cf07ed5a 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -155,7 +155,9 @@ except: HAVE_PSUTIL = False 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() import magic From f19599886532acb8420ab52d3ed5ba5e1649c0da Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 19:35:00 +0000 Subject: [PATCH 08/19] per-volume uid/gid; closes #265 --- README.md | 21 +++++++++++++++++++ copyparty/__main__.py | 2 ++ copyparty/authsrv.py | 17 ++++++++++++++- copyparty/bos/bos.py | 17 ++++++++++++--- copyparty/cfg.py | 4 ++++ copyparty/ftpd.py | 8 ++++---- copyparty/httpcli.py | 48 +++++++++++++++++++++---------------------- copyparty/smbd.py | 2 +- copyparty/tftpd.py | 9 +++++--- copyparty/th_srv.py | 4 ++-- copyparty/up2k.py | 17 ++++++++------- copyparty/util.py | 19 ++++++++++++----- tests/util.py | 3 +++ 13 files changed, 120 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 8dad60c6..83c47875 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ made in Norway 🇳🇴 * [periodic rescan](#periodic-rescan) - filesystem monitoring * [upload rules](#upload-rules) - set upload rules using volflags * [compress uploads](#compress-uploads) - files can be autocompressed on upload + * [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership * [other flags](#other-flags) * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload @@ -1649,6 +1650,26 @@ some examples, allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4` +## chmod and chown + +per-volume filesystem-permissions and ownership + +by default: +* all folders are chmod 755 +* files are usually chmod 644 (umask-defined) +* user/group is whatever copyparty is running as + +this can be configured per-volume: +* volflag `chmod_f` sets file permissions; default=`644` (usually) +* volflag `chmod_d` sets directory permissions; default=`755` +* volflag `uid` sets the owner user-id +* volflag `gid` sets the owner group-id + +notes: +* `gid` can only be set to one of the groups which the copyparty process is a member of +* `uid` can only be set if copyparty is running as root (i appreciate your faith) + + ## other flags * `:c,magic` enables filetype detection for nameless uploads, same as `--magic` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f6c20b8c..461a57e7 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1053,6 +1053,8 @@ 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("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)") ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)") + ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)") + ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)") ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)") ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)") ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 03dea807..89cab889 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -140,6 +140,8 @@ class Lim(object): self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry self.chmod_d = 0o755 + self.uid = self.gid = -1 + self.chown = False self.nups: dict[str, list[float]] = {} # num tracker self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list @@ -302,6 +304,8 @@ class Lim(object): # no branches yet; make one sub = os.path.join(path, "0") bos.mkdir(sub, self.chmod_d) + if self.chown: + os.chown(sub, self.uid, self.gid) else: # try newest branch only sub = os.path.join(path, str(dirs[-1])) @@ -317,6 +321,8 @@ class Lim(object): # make a branch sub = os.path.join(path, str(dirs[-1] + 1)) bos.mkdir(sub, self.chmod_d) + if self.chown: + os.chown(sub, self.uid, self.gid) ret = self.dive(sub, lvs - 1) if ret is None: raise Pebkac(500, "rotation bug") @@ -2181,7 +2187,7 @@ class AuthSrv(object): if vf not in vol.flags: 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(): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) @@ -2218,8 +2224,17 @@ class AuthSrv(object): if (is_d and zi != 0o755) or not is_d: free_umask = True + vol.flags.pop("chown", None) + if vol.flags["uid"] != -1 or vol.flags["gid"] != -1: + vol.flags["chown"] = True + vol.flags.pop("fperms", None) + if "chown" in vol.flags or vol.flags.get("chmod_f"): + vol.flags["fperms"] = True if vol.lim: vol.lim.chmod_d = vol.flags["chmod_d"] + vol.lim.chown = "chown" in vol.flags + vol.lim.uid = vol.flags["uid"] + vol.lim.gid = vol.flags["gid"] if vol.flags.get("og"): self.args.uqe = True diff --git a/copyparty/bos/bos.py b/copyparty/bos/bos.py index 3d98d0df..6c876e04 100644 --- a/copyparty/bos/bos.py +++ b/copyparty/bos/bos.py @@ -9,8 +9,11 @@ from . import path as path if True: # pylint: disable=using-constant-test from typing import Any, Optional -_ = (path,) -__all__ = ["path"] +MKD_755 = {"chmod_d": 0o755} +MKD_700 = {"chmod_d": 0o700} + +_ = (path, MKD_755, MKD_700) +__all__ = ["path", "MKD_755", "MKD_700"] # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" @@ -20,11 +23,15 @@ def chmod(p: str, mode: int) -> None: return os.chmod(fsenc(p), mode) +def chown(p: str, uid: int, gid: int) -> None: + return os.chown(fsenc(p), uid, gid) + + def listdir(p: str = ".") -> list[str]: return [fsdec(x) for x in os.listdir(fsenc(p))] -def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool: +def makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool: # os.makedirs does 777 for all but leaf; this does mode on all todo = [] bname = fsenc(name) @@ -37,9 +44,13 @@ def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool: if not exist_ok: os.mkdir(bname) # to throw return False + mode = vf["chmod_d"] + chown = "chown" in vf for zb in todo[::-1]: try: os.mkdir(zb, mode) + if chown: + os.chown(zb, vf["uid"], vf["gid"]) except: if os.path.isdir(zb): continue diff --git a/copyparty/cfg.py b/copyparty/cfg.py index cee8214b..2f75ab28 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -114,6 +114,8 @@ def vf_vmap() -> dict[str, str]: "unlist", "u2abort", "u2ts", + "uid", + "gid", "ups_who", "zip_who", "zipmaxn", @@ -175,6 +177,8 @@ flagcats = { "nodupe": "rejects existing files (instead of linking/cloning them)", "chmod_d=755": "unix-permission for new dirs/folders", "chmod_f=644": "unix-permission for new files", + "uid=573": "change owner of new files/folders to unix-user 573", + "gid=999": "change owner of new files/folders to unix-group 999", "sparse": "force use of sparse files, mainly for s3-backed storage", "nosparse": "deny use of sparse files, mainly for slow storage", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index c464450e..2f45c3f4 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -31,6 +31,7 @@ from .util import ( relchk, runhook, sanitize_fn, + set_fperms, vjoin, wunlink, ) @@ -262,8 +263,8 @@ class FtpFs(AbstractedFS): wunlink(self.log, ap, VF_CAREFUL) ret = open(fsenc(ap), mode, self.args.iobuf) - if w and "chmod_f" in vfs.flags: - os.fchmod(ret.fileno(), vfs.flags["chmod_f"]) + if w and "fperms" in vfs.flags: + set_fperms(ret, vfs.flags) return ret @@ -297,8 +298,7 @@ class FtpFs(AbstractedFS): def mkdir(self, path: str) -> None: ap, vfs, _ = self.rv2a(path, w=True) - chmod = vfs.flags["chmod_d"] - bos.makedirs(ap, chmod) # filezilla expects this + bos.makedirs(ap, vf=vfs.flags) # filezilla expects this def listdir(self, path: str) -> list[str]: vpath = join(self.cwd, path) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index f90f4d93..e4bfe45a 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -103,6 +103,7 @@ from .util import ( sanitize_vpath, sendfile_kern, sendfile_py, + set_fperms, stat_resource, ub64dec, ub64enc, @@ -2086,7 +2087,7 @@ class HttpCli(object): fdir, fn = os.path.split(fdir) rem, _ = vsplit(rem) - bos.makedirs(fdir, vfs.flags["chmod_d"]) + bos.makedirs(fdir, vf=vfs.flags) open_ka: dict[str, Any] = {"fun": open} open_a = ["wb", self.args.iobuf] @@ -2144,9 +2145,7 @@ class HttpCli(object): if nameless: fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip()) - params = {"suffix": suffix, "fdir": fdir} - if "chmod_f" in vfs.flags: - params["chmod"] = vfs.flags["chmod_f"] + params = {"suffix": suffix, "fdir": fdir, "vf": vfs.flags} if self.args.nw: params = {} fn = os.devnull @@ -2195,7 +2194,7 @@ class HttpCli(object): if self.args.nw: fn = os.devnull else: - bos.makedirs(fdir, vfs.flags["chmod_d"]) + bos.makedirs(fdir, vf=vfs.flags) path = os.path.join(fdir, fn) if not nameless: self.vpath = vjoin(self.vpath, fn) @@ -2327,7 +2326,7 @@ class HttpCli(object): if self.args.hook_v: log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem) fdir, self.vpath, fn, (vfs, rem) = x - bos.makedirs(fdir, vfs.flags["chmod_d"]) + bos.makedirs(fdir, vf=vfs.flags) path2 = os.path.join(fdir, fn) atomic_move(self.log, path, path2, vfs.flags) path = path2 @@ -2613,7 +2612,7 @@ class HttpCli(object): dst = vfs.canonical(rem) try: if not bos.path.isdir(dst): - bos.makedirs(dst, vfs.flags["chmod_d"]) + bos.makedirs(dst, vf=vfs.flags) except OSError as ex: self.log("makedirs failed %r" % (dst,)) if not bos.path.isdir(dst): @@ -3060,7 +3059,7 @@ class HttpCli(object): raise Pebkac(405, 'folder "/%s" already exists' % (vpath,)) try: - bos.makedirs(fn, vfs.flags["chmod_d"]) + bos.makedirs(fn, vf=vfs.flags) except OSError as ex: if ex.errno == errno.EACCES: raise Pebkac(500, "the server OS denied write-access") @@ -3102,8 +3101,8 @@ class HttpCli(object): with open(fsenc(fn), "wb") as f: f.write(b"`GRUNNUR`\n") - if "chmod_f" in vfs.flags: - os.fchmod(f.fileno(), vfs.flags["chmod_f"]) + if "fperms" in vfs.flags: + set_fperms(f, vfs.flags) vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") self.redirect(vpath, "?edit") @@ -3177,7 +3176,7 @@ class HttpCli(object): ) upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/") 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) zs = self.uparam.get("want") or self.headers.get("accept") or "" @@ -3210,7 +3209,7 @@ class HttpCli(object): if rnd: fname = rand_name(fdir, fname, rnd) - open_args = {"fdir": fdir, "suffix": suffix} + open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags} if "replace" in self.uparam: if not self.can_delete: @@ -3272,11 +3271,8 @@ class HttpCli(object): else: open_args["fdir"] = fdir - if "chmod_f" in vfs.flags: - open_args["chmod"] = vfs.flags["chmod_f"] - if p_file and not nullwrite: - bos.makedirs(fdir, vfs.flags["chmod_d"]) + bos.makedirs(fdir, vf=vfs.flags) # reserve destination filename f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix) @@ -3380,7 +3376,7 @@ class HttpCli(object): if nullwrite: fdir = ap2 = "" else: - bos.makedirs(fdir, vfs.flags["chmod_d"]) + bos.makedirs(fdir, vf=vfs.flags) atomic_move(self.log, abspath, ap2, vfs.flags) abspath = ap2 sz = bos.path.getsize(abspath) @@ -3501,8 +3497,8 @@ class HttpCli(object): ft = "{}:{}".format(self.ip, self.addr[1]) ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg) f.write(ft.encode("utf-8")) - if "chmod_f" in vfs.flags: - os.fchmod(f.fileno(), vfs.flags["chmod_f"]) + if "fperms" in vfs.flags: + set_fperms(f, vfs.flags) except Exception as ex: suf = "\nfailed to write the upload report: {}".format(ex) @@ -3553,7 +3549,7 @@ class HttpCli(object): lim = vfs.get_dbv(rem)[0].lim if lim: fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker) - bos.makedirs(fp, vfs.flags["chmod_d"]) + bos.makedirs(fp, vf=vfs.flags) fp = os.path.join(fp, fn) rem = "{}/{}".format(rp, fn).strip("/") @@ -3621,15 +3617,17 @@ class HttpCli(object): zs = ub64enc(zb).decode("ascii")[:24].lower() dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs) self.log("moving old version to %s/%s" % (dp, mfile2)) - if bos.makedirs(dp, vfs.flags["chmod_d"]): + if bos.makedirs(dp, vf=vfs.flags): with open(os.path.join(dp, "dir.txt"), "wb") as f: f.write(afsenc(vrd)) - if "chmod_f" in vfs.flags: - os.fchmod(f.fileno(), vfs.flags["chmod_f"]) + if "fperms" in vfs.flags: + set_fperms(f, vfs.flags) elif hist_cfg == "s": dp = os.path.join(mdir, ".hist") try: bos.mkdir(dp, vfs.flags["chmod_d"]) + if "chown" in vfs.flags: + bos.chown(dp, vfs.flags["uid"], vfs.flags["gid"]) hidedir(dp) except: pass @@ -3668,8 +3666,8 @@ class HttpCli(object): wunlink(self.log, fp, vfs.flags) with open(fsenc(fp), "wb", self.args.iobuf) as f: - if "chmod_f" in vfs.flags: - os.fchmod(f.fileno(), vfs.flags["chmod_f"]) + if "fperms" in vfs.flags: + set_fperms(f, vfs.flags) sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp) if lim: diff --git a/copyparty/smbd.py b/copyparty/smbd.py index d5098de5..2b9b3d77 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -320,7 +320,7 @@ class SMB(object): self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2) try: - bos.makedirs(ap2, vfs2.flags["chmod_d"]) + bos.makedirs(ap2, vf=vfs2.flags) except: pass diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 82ef3726..6f5726b3 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -45,6 +45,7 @@ from .util import ( exclude_dotfiles, min_ex, runhook, + set_fperms, undot, vjoin, vsplit, @@ -388,8 +389,8 @@ class Tftpd(object): a = (self.args.iobuf,) ret = open(ap, mode, *a, **ka) - if wr and "chmod_f" in vfs.flags: - os.fchmod(ret.fileno(), vfs.flags["chmod_f"]) + if wr and "fperms" in vfs.flags: + set_fperms(ret, vfs.flags) return ret @@ -398,7 +399,9 @@ class Tftpd(object): if "*" not in vfs.axs.uwrite: 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: # return bos.unlink(self._v2a("stat", vpath, *a)[1]) diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index e00c04e9..67413c10 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -269,8 +269,8 @@ class ThumbSrv(object): self.log("joined waiting room for %r" % (tpath,)) except: thdir = os.path.dirname(tpath) - chmod = 0o700 if self.args.free_umask else 0o755 - bos.makedirs(os.path.join(thdir, "w"), chmod) + chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755 + bos.makedirs(os.path.join(thdir, "w"), vf=chmod) inf_path = os.path.join(thdir, "dir.txt") if not bos.path.exists(inf_path): diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 907347f5..49bf21b7 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -916,7 +916,7 @@ class Up2k(object): for vol in vols: try: # 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) except Exception as ex: self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" @@ -3309,7 +3309,7 @@ class Up2k(object): reg, "up2k._get_volsize", ) - bos.makedirs(ap2, vfs.flags["chmod_d"]) + bos.makedirs(ap2, vf=vfs.flags) vfs.lim.nup(cj["addr"]) vfs.lim.bup(cj["addr"], cj["size"]) @@ -3445,7 +3445,7 @@ class Up2k(object): "wb", fdir=fdir, suffix="-%.6f-%s" % (ts, dip), - chmod=vf.get("chmod_f", -1), + vf=vf, ) f.close() return ret @@ -4304,7 +4304,7 @@ class Up2k(object): self.log(t, 1) 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( svn_dbv.realpath, srem_dbv @@ -4480,7 +4480,10 @@ class Up2k(object): vp = vjoin(dvp, rem) try: dvn, drem = self.vfs.get(vp, uname, False, True) - bos.mkdir(dvn.canonical(drem), 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: pass @@ -4550,7 +4553,7 @@ class Up2k(object): 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: dlabs = absreal(sabs) @@ -5062,7 +5065,7 @@ class Up2k(object): "wb", fdir=pdir, suffix="-%.6f-%s" % (job["t0"], dip), - chmod=vf.get("chmod_f", -1), + vf=vf, ) try: abspath = djoin(pdir, job["tnam"]) diff --git a/copyparty/util.py b/copyparty/util.py index cf07ed5a..58a883d5 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -1587,7 +1587,8 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str fun = kwargs.pop("fun", open) fdir = kwargs.pop("fdir", None) suffix = kwargs.pop("suffix", None) - chmod = kwargs.pop("chmod", -1) + vf = kwargs.pop("vf", None) + fperms = vf and "fperms" in vf if fname == os.devnull: return fun(fname, *args, **kwargs), fname @@ -1631,11 +1632,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str fp2 = os.path.join(fdir, fp2) with open(fsenc(fp2), "wb") as f2: f2.write(orig_name.encode("utf-8")) - if chmod >= 0: - os.fchmod(f2.fileno(), chmod) + if fperms: + set_fperms(f2, vf) - if chmod >= 0: - os.fchmod(f.fileno(), chmod) + if fperms: + set_fperms(f, vf) return f, fname @@ -2565,6 +2566,14 @@ def lsof(log: "NamedLogger", abspath: str) -> None: log("lsof failed; " + min_ex(), 3) +def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) -> None: + fno = f.fileno() + if "chmod_f" in vf: + os.fchmod(fno, vf["chmod_f"]) + if "chown" in vf: + os.fchown(fno, vf["uid"], vf["gid"]) + + def _fs_mvrm( log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] ) -> bool: diff --git a/tests/util.py b/tests/util.py index 17c1d06d..8027c372 100644 --- a/tests/util.py +++ b/tests/util.py @@ -152,6 +152,9 @@ 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" ka.update(**{k: None for k in ex.split()}) + 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()}) From 6069bc9b19d9d366a0bbba45b3525932e2cc01ff Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 19:38:33 +0000 Subject: [PATCH 09/19] mention optional idp persistence --- docs/idp.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/idp.md b/docs/idp.md index f3dc7e8c..c27a9355 100644 --- a/docs/idp.md +++ b/docs/idp.md @@ -9,9 +9,9 @@ in the copyparty `[global]` config, specify which headers to read client info fr # important notes -## IdP volumes are forgotten on shutdown +## by default, IdP volumes are forgotten on shutdown -IdP volumes, meaning dynamically-created volumes, meaning volumes that contain `${u}` or `${g}` in their URL, will be forgotten during a server restart and then "revived" when the volume's owner sends their first request after the restart +IdP volumes, meaning dynamically-created volumes, meaning volumes that contain `${u}` or `${g}` in their URL, will (by default) be forgotten during a server restart and then "revived" when the volume's owner sends their first request after the restart until each IdP volume is revived, it will inherit the permissions of its parent volume (if any) @@ -19,7 +19,17 @@ this means that, if an IdP volume is located inside a folder that is readable by and likewise -- if the IdP volume is inside a folder that is only accessible by certain users, but the IdP volume is configured to allow access from unauthenticated users, then the contents of the volume will NOT be accessible until it is revived -until this limitation is fixed (if ever), it is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the "strategic volumes" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf) +it is recommended to place IdP volumes inside an appropriate parent volume, so they can inherit acceptable permissions until their revival; see the "strategic volumes" at the bottom of [./examples/docker/idp/copyparty.conf](./examples/docker/idp/copyparty.conf) + +## but you can enable IdP volume persistence + +global-option `idp-store` can enable user/group persistence across restarts; + +* `idp-store: 1` (default) will log users into a database, but not actually "remember" them (the knowledge is ignored) +* `idp-store: 2` remembers usernames only +* `idp-store: 3` remembers usernames and their groups + +the reason why this is default-disabled, is because you may expect copyparty to forget about a user when you delete them from the IdP-server; this will not be the case any longer, you will need to click `view idp cache` in the controlpanel and manually remove the users you want gone ## Connecting webdav clients From e9684d402e987b7858be86952c9a971cf4b7f1ca Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 19:41:45 +0000 Subject: [PATCH 10/19] fix ipv6 cors-chk --- copyparty/httpcli.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index e4bfe45a..7d1a951b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1114,15 +1114,18 @@ class HttpCli(object): else: return True + host = self.host.lower() + if host.startswith("["): + if "]:" in host: + host = host.split("]:")[0] + "]" + else: + host = host.split(":")[0] + oh = self.out_headers origin = origin.lower() - good_origins = self.args.acao + [ - "%s://%s" - % ( - "https" if self.is_https else "http", - self.host.lower().split(":")[0], - ) - ] + proto = "https" if self.is_https else "http" + good_origins = self.args.acao + ["%s://%s" % (proto, host)] + if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins: good_origin = True bad_hdrs = ("",) From fb7cbc423b4832867cd9c36a156675d1a20cd908 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 19:43:47 +0000 Subject: [PATCH 11/19] shares: move all config to webroot --- copyparty/httpcli.py | 6 +++--- copyparty/web/shares.js | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 7d1a951b..0ff9c0ef 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5659,15 +5659,15 @@ class HttpCli(object): raise Pebkac(500, "sqlite3 not found on server; sharing is disabled") raise Pebkac(500, "server busy, cannot create share; please retry in a bit") + skey = self.uparam.get("skey") or self.vpath.split("/")[-1] + if self.args.shr_v: - self.log("handle_eshare: " + self.req) + self.log("handle_eshare: " + skey) cur = idx.get_shr() if not cur: raise Pebkac(400, "huh, sharing must be disabled in the server config...") - skey = self.vpath.split("/")[-1] - rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall() un = rows[0][0] if rows and rows[0] else "" diff --git a/copyparty/web/shares.js b/copyparty/web/shares.js index 86d4cc45..e3faf029 100644 --- a/copyparty/web/shares.js +++ b/copyparty/web/shares.js @@ -1,9 +1,11 @@ +var SRS = SR.trimEnd('/') + '/'; + var t = QSA('a[k]'); for (var a = 0; a < t.length; a++) t[a].onclick = rm; function rm() { - var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm', + var u = SRS + '?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')), xhr = new XHR(); xhr.open('POST', u, true); @@ -13,7 +15,7 @@ function rm() { function bump() { var k = this.closest('tr').getElementsByTagName('a')[2].getAttribute('k'), - u = SR + shr + uricom_enc(k) + '?eshare=' + this.value, + u = SRS + '?skey=' + uricom_enc(k) + '&eshare=' + this.value, xhr = new XHR(); xhr.open('POST', u, true); From 6016ec938844e37329a570f292eaeae9c3c7313e Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 20:30:18 +0000 Subject: [PATCH 12/19] connectpage: fix sharex --- copyparty/web/svcs.html | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html index c47d8403..a4b57746 100644 --- a/copyparty/web/svcs.html +++ b/copyparty/web/svcs.html @@ -240,14 +240,26 @@

ShareX

-

to upload screenshots using ShareX v12 or v15+, save this as copyparty.sxcu and run it:

+

to upload screenshots using ShareX v15+, save this as copyparty.sxcu and run it:

+ +
+                { "Version": "15.0.0", "Name": "copyparty",
+                "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
+                "Headers": {
+                    {% if accs %}"pw": "{{ pw }}", {% endif %}"accept": "url"
+                },
+                "DestinationType": "ImageUploader, TextUploader, FileUploader",
+                "Body": "MultipartFormData", "URL": "{response}",
+                "RequestMethod": "POST", "FileFormName": "f" }
+            
+ +

for ShareX v12 specifically, save this as copyparty.sxcu and run it:

                 { "Name": "copyparty",
                 "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
                 "Headers": {
-                    {% if accs %}"pw": "{{ pw }}",{% endif %}
-                    "accept": "url"
+                    {% if accs %}"pw": "{{ pw }}", {% endif %}"accept": "url"
                 },
                 "DestinationType": "ImageUploader, TextUploader, FileUploader",
                 "FileFormName": "f" }

From 9d32564c68124d60f0c72f05b7b09febb9083fb3 Mon Sep 17 00:00:00 2001
From: Raphael Guntersweiler 
Date: Wed, 30 Jul 2025 22:34:51 +0200
Subject: [PATCH 13/19] translate to german (#212)

* added german translation
---
 copyparty/web/browser.js | 628 ++++++++++++++++++++++++++++++++++++++-
 copyparty/web/splash.js  |  44 ++-
 2 files changed, 670 insertions(+), 2 deletions(-)

diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js
index f1368912..fc63bf9f 100644
--- a/copyparty/web/browser.js
+++ b/copyparty/web/browser.js
@@ -1884,9 +1884,635 @@ var Ls = {
 
 		"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<code>foo bar</code> = muss «foo» und «bar» enthalten,$N<code>foo -bar</code> = muss «foo» aber nicht «bar» enthalten,$N<code>^yana .opus$</code> = beginnt mit «yana» und ist «opus»-Datei$N<code>"try unite"</code> = genau «try unite» enthalten$N$NDatumsformat ist iso-8601, z.B.$N<code>2009-12-31</code> oder <code>2020-09-12 23:30:00</code>",
+		"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 [🎈]  (der einfache Uploader)

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 [🎈]  (der einfache Uploader)

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 <small>(um sie dann irgendwo anders einzufügen)</small>$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 📻copy)", + "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
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. <em>hashing</em> Geschw. & gesch. Restzeit", + "ut_etau": "durchschnittl. <em>upload</em> Geschw. & gesch. Restzeit", + "ut_etat": "durchschnittl. <em>total</em> 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
jedoch ist up2k gerade in einem anderen Browsertab aktiv.
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": '
okay, was gibts??
Eingabe = Dateien (1 oder mehr)\nESC = 1 Ordner (inkl. Unterordner)', + "u_nav_b": 'Dateien1 Ordner', + + "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™', + "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 "Ist die Datei auf dem Server gleich gross?", 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 "hochladen", 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 theoretisch 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 (<code>,sorthref</code>) 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  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 unzuverlässiger Verbindung aktivieren,$N❌ bei langsamer Verbindung deaktivieren\">vollstä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  (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 <code>0</code> = Standard 100% Lautstärke (unverändert)$N$Nwidth <code>1  </code> = Standard Stereo (unverändert)$Nwidth <code>0.5</code> = 50% Links-Rechts-Crossfeed$Nwidth <code>0  </code> = Mono$N$Nboost <code>-0.8</code> & width <code>10</code> = 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 Eingabe/OK zum Abspielen\nDrücke ESC/Abbrechen 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": '
zeige {0} von {1} Dateien; {2} anzeigen oder alle anzeigen
', + "fbd_all": '
zeige {0} von {1} Dateien; alle anzeigen
', + "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 .PARTIAL-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 .PARTIAL-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 Eingabe/OK für Zwischenablage\ndrücke ESC/Abbrechen 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 <>:"\\|?* durch japanische Fullwidth-Zeichen\">win", + "fr_slash": "Ersetzt / durch ein Zeichen, das keine neuen Ordner erstellt\">no /", + "fr_re": "Regex-Suchmuster für Originaldateinamen; Erfassungsgruppen können im Formatfeld unten als <code>(1)</code> und <code>(2)</code> usw. referenziert werden", + "fr_fmt": "inspiriert von foobar2000:$N<code>(title)</code> wird durch Songtitel ersetzt,$N<code>[(artist) - ](title)</code> überspringt [diesen] Teil falls Interpret leer$N<code>$lpad((tn),2,0)</code> 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 win und/oder no /\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": "Ich frage das letzte Mal! 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 dieses 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 dieses 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": '
Wähle, was eingefügt werden soll
Eingabe = {0} Dateien von «{1}» verschieben\nESC = {2} Dateien von deinem Gerät hochladen', + "fcp_both_m": '
Wähle, was eingefügt werden soll
Eingabe = {0} Dateien von «{1}» kopieren\nESC = {2} Dateien von deinem Gerät hochladen', + "fp_both_b": 'VerschiebenHochladen', + "fcp_both_b": 'KopierenHochladen', + + "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   ( 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<em>wenn aktiv: Datei / Ordner doppelklicken zum Öffnen</em>$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   (Leerzeichen-separiert)", + "s_f1": "Name enthält   (negieren mit -nope)", + "s_t1": "Tags enthält   (^=start, end=$)", + "s_a1": "spezifische Metadaten-Eigenschaften", + + "md_eshow": "Kann nicht rendern ", + "md_off": "[📜readme] 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 (slower)", + "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:  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": 'Warnung: Der Server ist grade zu beschäftigt, um unvollständige Uploads anzuzeigen; Drücke den "Neu laden"-Link in ein paar Sekunden', + "un_noc": 'Warnung: 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
{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 stattdessen bup benutzen', + "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 Potato UI wechseln (kann Upload-Geschw. verbessern)', + "u_depot": 'Zu fancy UI 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": "

Dateien:   {0} fertig,   {1} fehlgeschlagen,   {2} in Bearbeitung,   {3} ausstehend

", + "u_ever": "Dies ist der Basic Uploader; up2k benötigt mind.
Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1", + "u_su2k": 'Dies ist der Basic Uploader; up2k 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;\nOK/Eingabe drücken, um die folgenden Dateien zu überspringen.\nDrücke Abbrechen/ESC 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 Linux / MacOS / Android benutzt, könnte Firefox mit dieser Menge an Dateien crashen!\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 {1} 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-
Einbusen zu Vermeiden beim Wechsel auf andere Fenster/Tabs", + "u_fixed": "OK!  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": ' wechsle auf HTTPS (empfohlen) oder ', + "u_emtleak3": '', + "u_emtleakc": 'versuche folgendes:\n
  • drücke F5 um die Seite neu zu laden
  • deaktivere dann den  mt  Button in den  ⚙️ Einstellungen
  • und versuche den Upload nochmal.
Uploads werden etwas langsamer sein, aber man kann ja nicht alles haben.\nSorry für die Umstände !\n\nPS: Chrome v107 hat ein Bugfix dafür', + "u_emtleakf": 'versuche folgendes:\n
  • drücke F5 um die Seite neu zu laden
  • aktivere dann 🥔 (potato) im Upload UI
  • und versuche den Upload nochmal
\nPS: Firefox hat hoffentlich irgendwann ein Bugfix', + "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 connections-per-server in about:config", + "u_tu": '

WARNUNG: Turbo aktiviert,  Client könnte unvollständige Uploads verpassen und nicht wiederholen; siehe Turbo-Button Tooltip

', + "u_ts": '

WARNUNG: Turbo aktiviert,  Suchresultate können inkorrekt sein; siehe Turbo-Button Tooltip

', + "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 min (or h)', + "u_life_est": 'Upload wird gelöscht ---', + "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) langmod(); diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index f0c38b54..c2e60eb6 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -92,7 +92,49 @@ var Ls = { "ae1": "正在下载:", //m "af1": "显示最近上传的文件", //m "ag1": "查看已知 IdP 用户", //m - } + }, + + "deu": { + "a1": "Neu laden", + "b1": "Tach, wie geht's?   (Du bist nicht angemeldet)", + "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  ┐( ´ -`)┌", + "o1": 'or maybe you don\'t have access -- try a password or go home', + "p1": "403 Verboten  ~┻━┻", + "q1": 'Benutze ein Passwort oder gehe zur Homepage', + "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) From 4f1eb89382ded1fe7368d920e243219cd5b40a45 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 21:05:37 +0000 Subject: [PATCH 14/19] just moving some stuff around, not foreshadowing --- copyparty/__main__.py | 4 ++-- copyparty/svchub.py | 6 +++--- copyparty/util.py | 28 +++++++++++++++++----------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 461a57e7..f7e93b24 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -53,13 +53,13 @@ from .util import ( PYFTPD_VER, RAM_AVAIL, RAM_TOTAL, + RE_ANSI, SQLITE_VER, UNPLICATIONS, URL_BUG, URL_PRJ, Daemon, align_tab, - ansi_re, b64enc, dedent, has_resource, @@ -167,7 +167,7 @@ def lprint(*a: Any, **ka: Any) -> None: txt: str = " ".join(unicode(x) for x in a) + eol printed.append(txt) if not VT100: - txt = ansi_re.sub("", txt) + txt = RE_ANSI.sub("", txt) print(txt, end="", **ka) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 751fd566..73855bf1 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -51,6 +51,7 @@ from .util import ( HAVE_PSUTIL, HAVE_SQLITE3, HAVE_ZMQ, + RE_ANSI, URL_BUG, UTC, VERSIONS, @@ -60,7 +61,6 @@ from .util import ( HMaccas, ODict, alltrace, - ansi_re, build_netmap, expat_ver, gzip, @@ -1409,9 +1409,9 @@ class SvcHub(object): if self.no_ansi: fmt = "%s %-21s %s\n" if "\033" in msg: - msg = ansi_re.sub("", msg) + msg = RE_ANSI.sub("", msg) if "\033" in src: - src = ansi_re.sub("", src) + src = RE_ANSI.sub("", src) elif c: if isinstance(c, int): msg = "\033[3%sm%s\033[0m" % (c, msg) diff --git a/copyparty/util.py b/copyparty/util.py index 58a883d5..ae968777 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -243,7 +243,17 @@ except: BITNESS = struct.calcsize("P") * 8 -ansi_re = re.compile("\033\\[[^mK]*[mK]") +RE_ANSI = re.compile("\033\\[[^mK]*[mK]") +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") @@ -488,11 +498,11 @@ def read_ram() -> tuple[float, float]: with open("/proc/meminfo", "rb", 0x10000) as f: zsl = f.read(0x10000).decode("ascii", "replace").split("\n") - p = re.compile("^MemTotal:.* kB") + p = RE_MEMTOTAL zs = next((x for x in zsl if p.match(x))) a = int((int(zs.split()[1]) / 0x100000) * 100) / 100 - p = re.compile("^MemAvailable:.* kB") + p = RE_MEMAVAIL zs = next((x for x in zsl if p.match(x))) b = int((int(zs.split()[1]) / 0x100000) * 100) / 100 except: @@ -1698,14 +1708,10 @@ class MultipartParser(object): self.args = args self.headers = http_headers - self.re_ctype = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE) - self.re_cdisp = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE) - self.re_cdisp_field = re.compile( - r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE - ) - self.re_cdisp_file = re.compile( - r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE - ) + self.re_ctype = RE_CTYPE + self.re_cdisp = RE_CDISP + self.re_cdisp_field = RE_CDISP_FIELD + self.re_cdisp_file = RE_CDISP_FILE self.boundary = b"" self.gen: Optional[ From b7ca6f4a66202ced70ebd87df169aebb41ebb81d Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 21:07:47 +0000 Subject: [PATCH 15/19] try to fix #300 the importlib stuff broke early versions of py2.7 --- copyparty/util.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/copyparty/util.py b/copyparty/util.py index ae968777..4e5cbfbe 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -4186,7 +4186,14 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]: stream = codecs.getreader(enc)(stream) return stream - return open(os.path.join(E.mod, name), mode, encoding=enc) + ap = os.path.join(E.mod, name) + + if PY2: + import codecs + + return codecs.open(ap, "r", encoding=enc) # type: ignore + + return open(ap, mode, encoding=enc) class Pebkac(Exception): From a8705e611d05eeb22be5d3d7d9ab5c020fe54c62 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 21:19:39 +0000 Subject: [PATCH 16/19] 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=` huge thanks to @Ju0x for finding and reporting this! --- copyparty/httpcli.py | 3 ++- copyparty/util.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 0ff9c0ef..fa6d7418 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -81,6 +81,7 @@ from .util import ( html_escape, humansize, ipnorm, + json_hesc, justcopy, load_resource, loadpy, @@ -5595,7 +5596,7 @@ class HttpCli(object): self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") return True - html = self.j2s("rups", this=self, v=jtxt) + html = self.j2s("rups", this=self, v=json_hesc(jtxt)) self.reply(html.encode("utf-8"), status=200) return True diff --git a/copyparty/util.py b/copyparty/util.py index 4e5cbfbe..94e0f249 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -2253,6 +2253,10 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]: return ret +def json_hesc(s: str) -> str: + return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026") + + def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str: """html.escape but also newlines""" s = s.replace("&", "&").replace("<", "<").replace(">", ">") From 13d5631b4897423c3f97dd02b24691f3b1e2c138 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 21:26:27 +0000 Subject: [PATCH 17/19] more escapes in case --- copyparty/authsrv.py | 3 ++- copyparty/httpcli.py | 22 ++++++++++------------ copyparty/util.py | 7 +++++++ copyparty/web/browser.html | 4 ++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 89cab889..28441114 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -33,6 +33,7 @@ from .util import ( afsenc, get_df, humansize, + json_hesc, min_ex, odfusion, read_utf8, @@ -2785,7 +2786,7 @@ class AuthSrv(object): "lifetime": vn.js_ls["lifetime"], "u2sort": self.args.u2sort, } - vn.js_htm = json.dumps(js_htm) + vn.js_htm = json_hesc(json.dumps(js_htm)) vols = list(vfs.all_nodes.values()) if enshare: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index fa6d7418..bdbaced2 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -79,6 +79,7 @@ from .util import ( hidedir, html_bescape, html_escape, + html_sh_esc, humansize, ipnorm, json_hesc, @@ -4912,11 +4913,8 @@ class HttpCli(object): else: rip = host - # safer than html_escape/quotep since this avoids both XSS and shell-stuff - pw = re.sub(r"[<>&$?`\"']", "_", self.pw or "hunter2") - vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/") - pw = pw.replace(" ", "%20") - vp = vp.replace(" ", "%20") + vp = (self.uparam["hc"] or "").lstrip("/") + pw = self.pw or "hunter2" if pw in self.asrv.sesa: pw = "hunter2" @@ -4925,14 +4923,14 @@ class HttpCli(object): args=self.args, accs=bool(self.asrv.acct), s="s" if self.is_https else "", - rip=rip, - ep=ep, - vp=vp, - rvp=vjoin(self.args.R, vp), - host=host, - hport=hport, + rip=html_sh_esc(rip), + ep=html_sh_esc(ep), + vp=html_sh_esc(vp), + rvp=html_sh_esc(vjoin(self.args.R, vp)), + host=html_sh_esc(host), + hport=html_sh_esc(hport), aname=aname, - pw=pw, + pw=html_sh_esc(pw), ) self.reply(html.encode("utf-8")) return True diff --git a/copyparty/util.py b/copyparty/util.py index 94e0f249..d081378a 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -244,6 +244,7 @@ except: 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( @@ -2253,6 +2254,12 @@ def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]: return ret +def html_sh_esc(s: str) -> str: + s = re.sub(RE_HTML_SH, "_", s).replace(" ", "%20") + s = s.replace("\r", "_").replace("\n", "_") + return s + + def json_hesc(s: str) -> str: return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026") diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index a642e9a9..05bdacbc 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -109,8 +109,8 @@ {%- for f in files %} {{ f.lead }}{{ f.name|e }}{{ f.sz }} {%- if f.tags is defined %} - {%- for k in taglist %}{{ f.tags[k] }}{%- endfor %} -{%- endif %}{{ f.ext }}{{ f.dt }} + {%- for k in taglist %}{{ f.tags[k]|e }}{%- endfor %} +{%- endif %}{{ f.ext|e }}{{ f.dt }} {%- endfor %} From daa44be1a595374cddecde6c2de5f925fbe8fd92 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 21:31:54 +0000 Subject: [PATCH 18/19] v1.18.7 --- copyparty/__main__.py | 2 +- copyparty/__version__.py | 4 ++-- copyparty/web/browser.js | 2 +- scripts/pyinstaller/build.sh | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f7e93b24..97944c13 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1557,7 +1557,7 @@ def add_ui(ap, retry): 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("--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 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("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)") ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable") ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)") ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])") diff --git a/copyparty/__version__.py b/copyparty/__version__.py index 3cbb3b4a..2006cc7a 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 18, 6) +VERSION = (1, 18, 7) CODENAME = "logtail" -BUILD_DT = (2025, 7, 28) +BUILD_DT = (2025, 7, 30) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index fc63bf9f..59b1ab5f 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -2393,7 +2393,7 @@ var Ls = { "fu_xe2": "404: Datei nicht gefunden??", "fz_tar": "Unkomprimierte GNU TAR-Datei (Linux / Mac)", - "fz_pax": "Unkomprimierte pax-format TAR-Datei (slower)", + "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)", diff --git a/scripts/pyinstaller/build.sh b/scripts/pyinstaller/build.sh index bb1d477e..a444ff94 100644 --- a/scripts/pyinstaller/build.sh +++ b/scripts/pyinstaller/build.sh @@ -14,7 +14,6 @@ clean=--clean uname -s | grep WOW64 && m=64 || m=32 uname -s | grep NT-10 && w10=1 || w7=1 -[ $w7 ] && export PRTY_NO_MAGIC=1 [ $w7 ] && [ -e up2k.sh ] && [ ! "$1" ] && ./up2k.sh [ $w7 ] && pyv=37 || pyv=313 From 5df2cbe5d76db8aee7d37a92b5a2670ef1084044 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 30 Jul 2025 21:59:58 +0000 Subject: [PATCH 19/19] update pkgs to 1.18.7 --- contrib/package/arch/PKGBUILD | 4 ++-- contrib/package/nix/copyparty/pin.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 959a9821..2fe085ff 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: icxes pkgname=copyparty -pkgver="1.18.6" +pkgver="1.18.7" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -22,7 +22,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}.d/init" ) -sha256sums=("80762d91ac88815e73d0ca2806c6391dcf8ccd521bc402cc312349f3bc8e8b28") +sha256sums=("6738f623905276e8664bd8791f1497d0c61f8816c6608e76c4d9097a12849170") build() { cd "${srcdir}/${pkgname}-${pkgver}" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index eae921c7..966def8e 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.18.6/copyparty-sfx.py", - "version": "1.18.6", - "hash": "sha256-No89mzKHHZZH19ws9dqfvQO0pnZw7jKDMGhNa4LOFlY=" + "url": "https://github.com/9001/copyparty/releases/download/v1.18.7/copyparty-sfx.py", + "version": "1.18.7", + "hash": "sha256-GRoiNnYRWNcq5ITywPv7bK4pdgTey6Or1cZqyE2XDf4=" } \ No newline at end of file