Repository: 9001/copyparty Branch: hovudstraum Commit: 2cebda32973d Files: 448 Total size: 4.6 MB Directory structure: gitextract_364x004l/ ├── .eslintrc.json ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ └── something-else.md │ ├── branch-rename.md │ └── pull_request_template.md ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── launch.py │ ├── settings.json │ └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin/ │ ├── README.md │ ├── bubbleparty.sh │ ├── dbtool.py │ ├── handlers/ │ │ ├── README.md │ │ ├── caching-proxy.py │ │ ├── ip-ok.py │ │ ├── never404.py │ │ ├── nooo.py │ │ ├── randpic.py │ │ ├── redirect.py │ │ └── sorry.py │ ├── hooks/ │ │ ├── README.md │ │ ├── discord-announce.py │ │ ├── image-noexif.py │ │ ├── import-me.py │ │ ├── into-the-cache-it-goes.py │ │ ├── msg-log.py │ │ ├── notify.py │ │ ├── notify2.py │ │ ├── podcast-normalizer.py │ │ ├── qbittorrent-magnet.py │ │ ├── reject-and-explain.py │ │ ├── reject-extension.py │ │ ├── reject-mimetype.py │ │ ├── reject-ramdisk.py │ │ ├── reloc-by-ext.py │ │ ├── usb-eject.js │ │ ├── usb-eject.py │ │ ├── wget-i.py │ │ ├── wget.py │ │ ├── xiu-sha.py │ │ └── xiu.py │ ├── mtag/ │ │ ├── README.md │ │ ├── audio-bpm.py │ │ ├── audio-key-slicing.py │ │ ├── audio-key.py │ │ ├── cksum.py │ │ ├── exe.py │ │ ├── file-ext.py │ │ ├── geotag.py │ │ ├── guestbook-read.py │ │ ├── guestbook.py │ │ ├── image-noexif.py │ │ ├── install-deps.sh │ │ ├── media-hash.py │ │ ├── mousepad.py │ │ ├── rclone-upload.py │ │ ├── res/ │ │ │ ├── twitter-unmute.user.js │ │ │ ├── yt-ipr.conf │ │ │ └── yt-ipr.user.js │ │ ├── sleep.py │ │ ├── very-bad-idea.py │ │ ├── vidchk.py │ │ ├── wget.py │ │ └── yt-ipr.py │ ├── partyfuse-streaming.py │ ├── partyfuse.py │ ├── partyfuse2.py │ ├── partyjournal.py │ ├── prisonparty.sh │ ├── u2c.py │ ├── unforget.py │ ├── up2k.sh │ └── zmq-recv.py ├── contrib/ │ ├── README.md │ ├── apache/ │ │ └── copyparty.conf │ ├── cfssl.sh │ ├── copyparty.bat │ ├── explorer-nothumbs-nofoldertypes.reg │ ├── flameshot.sh │ ├── haproxy/ │ │ └── copyparty.conf │ ├── index.html │ ├── ios/ │ │ └── upload-to-copyparty.shortcut │ ├── ishare.iscu │ ├── lighttpd/ │ │ ├── subdomain.conf │ │ └── subpath.conf │ ├── media-osd-bgone.ps1 │ ├── nginx/ │ │ └── copyparty.conf │ ├── nixos/ │ │ └── modules/ │ │ └── copyparty.nix │ ├── openrc/ │ │ └── copyparty │ ├── package/ │ │ ├── arch/ │ │ │ └── PKGBUILD │ │ ├── makedeb-mpr/ │ │ │ ├── PKGBUILD │ │ │ ├── copyparty.conf │ │ │ ├── copyparty.service │ │ │ ├── index.md │ │ │ └── prisonparty.service │ │ ├── nix/ │ │ │ ├── copyparty/ │ │ │ │ ├── default.nix │ │ │ │ ├── pin.json │ │ │ │ └── update.py │ │ │ ├── overlay.nix │ │ │ └── partftpy/ │ │ │ ├── default.nix │ │ │ ├── pin.json │ │ │ └── update.py │ │ └── rpm/ │ │ └── copyparty.spec │ ├── plugins/ │ │ ├── README.md │ │ ├── banner.js │ │ ├── browser-icons.css │ │ ├── graft-thumbs.js │ │ ├── meadup.js │ │ ├── minimal-up2k.html │ │ ├── minimal-up2k.js │ │ ├── quickmove.js │ │ ├── rave.js │ │ ├── up2k-hook-ytid.js │ │ └── up2k-hooks.js │ ├── podman-systemd/ │ │ ├── README.md │ │ ├── copyparty.conf │ │ └── copyparty.container │ ├── rc/ │ │ └── copyparty │ ├── send-to-cpp.contextlet.json │ ├── setup-ashell.sh │ ├── sharex.sxcu │ ├── sharex12.sxcu │ ├── systemd/ │ │ ├── cfssl.service │ │ ├── copyparty-user.service │ │ ├── copyparty.conf │ │ ├── copyparty.example.conf │ │ ├── copyparty.service │ │ ├── copyparty@.service │ │ ├── index.md │ │ ├── prisonparty.service │ │ └── prisonparty@.service │ ├── themes/ │ │ └── bsod.css │ ├── traefik/ │ │ └── copyparty.yaml │ ├── webdav-cfg.bat │ ├── windows/ │ │ └── copyparty-ctmp.bat │ └── zfs-tune.py ├── copyparty/ │ ├── __init__.py │ ├── __main__.py │ ├── __version__.py │ ├── authsrv.py │ ├── bos/ │ │ ├── __init__.py │ │ ├── bos.py │ │ └── path.py │ ├── broker_mp.py │ ├── broker_mpw.py │ ├── broker_thr.py │ ├── broker_util.py │ ├── cert.py │ ├── cfg.py │ ├── dxml.py │ ├── fsutil.py │ ├── ftpd.py │ ├── httpcli.py │ ├── httpconn.py │ ├── httpsrv.py │ ├── ico.py │ ├── mdns.py │ ├── metrics.py │ ├── mtag.py │ ├── multicast.py │ ├── pwhash.py │ ├── qrkode.py │ ├── res/ │ │ ├── __init__.py │ │ └── insecure.pem │ ├── sftpd.py │ ├── smbd.py │ ├── ssdp.py │ ├── star.py │ ├── stolen/ │ │ ├── __init__.py │ │ ├── dnslib/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── bimap.py │ │ │ ├── bit.py │ │ │ ├── buffer.py │ │ │ ├── dns.py │ │ │ ├── label.py │ │ │ ├── lex.py │ │ │ └── ranges.py │ │ ├── ifaddr/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── _posix.py │ │ │ ├── _shared.py │ │ │ └── _win32.py │ │ ├── qrcodegen.py │ │ └── surrogateescape.py │ ├── sutil.py │ ├── svchub.py │ ├── szip.py │ ├── tcpsrv.py │ ├── tftpd.py │ ├── th_cli.py │ ├── th_srv.py │ ├── u2idx.py │ ├── up2k.py │ ├── util.py │ └── web/ │ ├── Makefile │ ├── Makefile.s1 │ ├── a/ │ │ └── __init__.py │ ├── baguettebox.js │ ├── browser.css │ ├── browser.html │ ├── browser.js │ ├── browser2.html │ ├── cf.html │ ├── dbg-audio.js │ ├── idp.html │ ├── md.css │ ├── md.html │ ├── md.js │ ├── md2.css │ ├── md2.js │ ├── mde.css │ ├── mde.html │ ├── mde.js │ ├── msg.html │ ├── opds.xml │ ├── opds_osd.xml │ ├── rups.css │ ├── rups.html │ ├── rups.js │ ├── shares.css │ ├── shares.html │ ├── shares.js │ ├── splash.css │ ├── splash.html │ ├── splash.js │ ├── svcs.html │ ├── svcs.js │ ├── tl/ │ │ ├── chi.js │ │ ├── cze.js │ │ ├── deu.js │ │ ├── epo.js │ │ ├── fin.js │ │ ├── fra.js │ │ ├── grc.js │ │ ├── hun.js │ │ ├── ita.js │ │ ├── jpn.js │ │ ├── kor.js │ │ ├── nld.js │ │ ├── nno.js │ │ ├── nor.js │ │ ├── pol.js │ │ ├── por.js │ │ ├── rus.js │ │ ├── spa.js │ │ ├── swe.js │ │ ├── tur.js │ │ ├── ukr.js │ │ └── vie.js │ ├── ui.css │ ├── up2k.js │ ├── util.js │ └── w.hash.js ├── docs/ │ ├── README.md │ ├── TODO.md │ ├── bad-codecs.md │ ├── biquad.html │ ├── bufsize.txt │ ├── changelog.md │ ├── chungus.conf │ ├── chunksizes.py │ ├── copyparty.d/ │ │ ├── foo/ │ │ │ ├── another.conf │ │ │ └── sibling.conf │ │ ├── more-users/ │ │ │ ├── david.conf │ │ │ └── james.conf │ │ └── some.conf │ ├── cursed-usecases/ │ │ └── README.md │ ├── design.txt │ ├── devnotes.md │ ├── example.conf │ ├── example2.conf │ ├── examples/ │ │ ├── README.md │ │ ├── docker/ │ │ │ ├── basic-docker-compose/ │ │ │ │ ├── copyparty.conf │ │ │ │ └── docker-compose.yml │ │ │ ├── idp/ │ │ │ │ └── copyparty.conf │ │ │ ├── idp-authelia-traefik/ │ │ │ │ ├── README.md │ │ │ │ ├── authelia/ │ │ │ │ │ ├── configuration.yml │ │ │ │ │ └── users_database.yml │ │ │ │ ├── cpp/ │ │ │ │ │ └── copyparty.conf │ │ │ │ └── docker-compose.yml │ │ │ ├── idp-authentik-traefik/ │ │ │ │ ├── README.md │ │ │ │ ├── based-on/ │ │ │ │ │ ├── docker-compose-authentik.yml │ │ │ │ │ └── docker-compose-traefik.yml │ │ │ │ ├── cpp/ │ │ │ │ │ └── copyparty.conf │ │ │ │ └── docker-compose.yml │ │ │ └── portainer.md │ │ └── windows.md │ ├── hls.html │ ├── idp.md │ ├── lics.txt │ ├── multisearch.html │ ├── music-analysis.sh │ ├── notes.bat │ ├── notes.md │ ├── notes.sh │ ├── nuitka.txt │ ├── pretend-youre-qnap.patch │ ├── protocol-reference.sh │ ├── rclone.md │ ├── rice/ │ │ ├── README.md │ │ └── rtl.patch │ ├── synology-dsm.md │ ├── tcp-debug.sh │ ├── unirange.py │ ├── up2k.txt │ ├── versus.md │ └── xff.md ├── flake.nix ├── pyproject.toml ├── scripts/ │ ├── bench/ │ │ └── filehash.sh │ ├── copyparty-android.sh │ ├── copyparty-repack.sh │ ├── deps-docker/ │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── busy-mp3.sh │ │ ├── codemirror.patch │ │ ├── easymde-ln.patch │ │ ├── easymde-marked6.patch │ │ ├── easymde.patch │ │ ├── genprism.py │ │ ├── genprism.sh │ │ ├── markdown-it.patch │ │ ├── marked-ln.patch │ │ ├── marked.patch │ │ ├── mini-fa.css │ │ ├── mini-fa.sh │ │ ├── shiftbase.py │ │ ├── showdown.patch │ │ └── zopfli.makefile │ ├── docker/ │ │ ├── Dockerfile.ac │ │ ├── Dockerfile.dj │ │ ├── Dockerfile.djd │ │ ├── Dockerfile.djf │ │ ├── Dockerfile.djff │ │ ├── Dockerfile.dju │ │ ├── Dockerfile.im │ │ ├── Dockerfile.iv │ │ ├── Dockerfile.min │ │ ├── Dockerfile.min.pip │ │ ├── Makefile │ │ ├── README.md │ │ ├── base/ │ │ │ ├── Dockerfile.zlibng │ │ │ ├── Makefile │ │ │ ├── arbeidspakke.sh │ │ │ ├── build-no265.sh │ │ │ ├── patch/ │ │ │ │ └── ffmpeg/ │ │ │ │ ├── aac-lc-only.patch │ │ │ │ └── libavcodec/ │ │ │ │ ├── aacps.c │ │ │ │ ├── aacsbr.c │ │ │ │ ├── aacsbr_fixed.c │ │ │ │ └── aacsbrdata.h │ │ │ └── verchk.sh │ │ ├── devnotes.md │ │ ├── innvikler.sh │ │ └── make.sh │ ├── fusefuzz.py │ ├── genhelp.sh │ ├── genlic.py │ ├── help2html.py │ ├── help2txt.sh │ ├── install-githooks.sh │ ├── lics/ │ │ ├── 1.r13 │ │ ├── 2.r13 │ │ ├── 3.r13 │ │ ├── 4.r13 │ │ ├── 5.r13 │ │ ├── 6.r13 │ │ ├── README.md │ │ └── rot.py │ ├── logpack.sh │ ├── make-pypi-release.sh │ ├── make-pyz.sh │ ├── make-rpm.sh │ ├── make-sfx.sh │ ├── make-tgz-release.sh │ ├── patches/ │ │ ├── pyftpdlib-fe80.patch │ │ └── pyftpdlib-win313.patch │ ├── prep.sh │ ├── profile.py │ ├── py2/ │ │ └── queue/ │ │ └── __init__.py │ ├── pyinstaller/ │ │ ├── README.md │ │ ├── build.sh │ │ ├── depchk.sh │ │ ├── deps.sha512 │ │ ├── deps.txt │ │ ├── ffmpeg.patch │ │ ├── ffmpeg.txt │ │ ├── icon.sh │ │ ├── loader.py │ │ ├── loader.rc │ │ ├── notes.txt │ │ ├── up2k.rc │ │ ├── up2k.sh │ │ ├── up2k.spec │ │ └── up2k.spec.sh │ ├── rls.sh │ ├── run-tests.sh │ ├── sfx.ls │ ├── sfx.py │ ├── sfx.sh │ ├── speedtest-fs.py │ ├── strip_hints/ │ │ └── a.py │ ├── test/ │ │ ├── ptrav.py │ │ ├── race.py │ │ ├── smoketest.py │ │ └── tftp.sh │ ├── tl/ │ │ ├── 1.sh │ │ └── 1.txt │ ├── tl.js │ ├── tl.py │ ├── tlcheck.sh │ ├── toc.sh │ ├── uncomment.py │ └── ziploader.py ├── setup.py └── tests/ ├── __init__.py ├── ptrav.py ├── res/ │ └── idp/ │ ├── 1.conf │ ├── 2.conf │ ├── 3.conf │ ├── 4.conf │ ├── 5.conf │ ├── 6.conf │ ├── 7.conf │ └── 8.conf ├── run.py ├── test_cp.py ├── test_dedup.py ├── test_dots.py ├── test_dxml.py ├── test_hooks.py ├── test_httpcli.py ├── test_idp.py ├── test_metrics.py ├── test_mv.py ├── test_shr.py ├── test_utils.py ├── test_vfs.py ├── test_webdav.py └── util.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "es2021": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 12 }, "rules": { } } ================================================ FILE: .gitattributes ================================================ * text eol=lf *.reg text eol=crlf *.png binary *.gif binary *.gz binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '9001' --- ### Describe the bug a description of what the bug is ### To Reproduce List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it ### Expected behavior a description of what you expected to happen ### Screenshots if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^) ### Server details (if you are using docker/podman) remove the ones that are not relevant: * **server OS / version:** * **how you're running copyparty:** (docker/podman/something-else) * **docker image:** (variant, version, and arch if you know) * **copyparty arguments and/or config-file:** ### Server details (if you're NOT using docker/podman) remove the ones that are not relevant: * **server OS / version:** * **what copyparty did you grab:** (sfx/exe/pip/arch/...) * **how you're running it:** (in a terminal, as a systemd-service, ...) * run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line: * **copyparty arguments and/or config-file:** ### Client details if the issue is possibly on the client-side, then mention some of the following: * the device type and model: * OS version: * browser version: ### The rest of the stack if you are connecting directly to copyparty then that's cool, otherwise please mention everything else between copyparty and the browser (reverseproxy, tunnels, etc.) ### Server log if the issue might be server-related, include everything that appears in the copyparty log during startup, and also anything else you think might be relevant ### Additional context any other context about the problem here ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '9001' --- **is your feature request related to a problem? Please describe.** a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]` **Describe the idea / solution you'd like** a description of what you want to happen **Describe any alternatives you've considered** a description of any alternative solutions or features you've considered **Additional context** add any other context or screenshots about the feature request here ================================================ FILE: .github/ISSUE_TEMPLATE/something-else.md ================================================ --- name: Something else about: "┐(゚∀゚)┌" title: '' labels: '' assignees: '' --- ================================================ FILE: .github/branch-rename.md ================================================ modernize your local checkout of the repo like so, ```sh git branch -m master hovudstraum git fetch origin git branch -u origin/hovudstraum hovudstraum git remote set-head origin -a ``` ================================================ FILE: .github/pull_request_template.md ================================================ To show that your contribution is compatible with the MIT License, please include the following text somewhere in this PR description: This PR complies with the DCO; https://developercertificate.org/ ================================================ FILE: .gitignore ================================================ # python __pycache__/ *.py[cod] *$py.class MANIFEST.in MANIFEST copyparty.egg-info/ .venv/ /buildenv/ /build/ /dist/ /py2/ /sfx* /pyz/ /unt/ /log/ # ide *.sublime-workspace # winmerge *.bak # apple pls .DS_Store # derived copyparty/res/COPYING.txt copyparty/web/deps/ srv/ scripts/docker/base/b/ scripts/docker/base/cver* scripts/docker/base/test-aac/ scripts/docker/base/whl/ scripts/docker/i/ scripts/deps-docker/uncomment.py contrib/package/arch/pkg/ contrib/package/arch/src/ # state/logs up.*.txt .hist/ scripts/docker/*.out scripts/docker/*.err /perf.* # nix build output link result result-* # IDEA config .idea/ ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Run copyparty", "type": "debugpy", "request": "launch", "module": "copyparty", "console": "integratedTerminal", "cwd": "${workspaceFolder}", "justMyCode": false, "env": { "PYDEVD_DISABLE_FILE_VALIDATION": "1", "PYTHONWARNINGS": "always" //error }, "args": [ //"-nw", // no-write; for testing uploads without writing to disk //"-q", // quiet; speedboost when console output is not needed // # increase debugger performance: //"no-htp", //"hash-mt=0", //"mtag-mt=1", //"th-mt=1", // # listen for FTP and TFTP "--ftp=3921", "--ftp-pr=12000-12099", "--tftp=3969", // # listen on all IPv6, all IPv4, and unix-socket "-i::,unix:777:a.sock", // # misc "--dedup", "-e2dsa", "-e2ts", "--rss", "--shr=/shr", "--stats", "-z", // # users + volumes "-aed:wark", "-vdist:dist:r", "-vsrv::r:rw,ed", "-vsrv/junk:junk:r:A,ed", "--ver" ] }, { "name": "Run active unit test", "type": "debugpy", "request": "launch", "module": "unittest", "console": "integratedTerminal", "cwd": "${workspaceFolder}", "args": [ "-v", "${file}" ] }, { "name": "Python: Current File", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false } ] } ================================================ FILE: .vscode/launch.py ================================================ #!/usr/bin/env python3 # takes arguments from launch.json # is used by no_dbg in tasks.json # launches 10x faster than mspython debugpy # and is stoppable with ^C import re import os import sys print(sys.executable) import json5 import shlex import subprocess as sp with open(".vscode/launch.json", "r", encoding="utf-8") as f: tj = f.read() oj = json5.loads(tj) argv = oj["configurations"][0]["args"] try: sargv = " ".join([shlex.quote(x) for x in argv]) print(sys.executable + " -m copyparty " + sargv + "\n") except: pass argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv] sfx = "" if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): sfx = sys.argv[1] sys.argv = [sys.argv[0]] + sys.argv[2:] argv += sys.argv[1:] if sfx: argv = [sys.executable, sfx] + argv sp.check_call(argv) elif re.search(" -j ?[0-9]", " ".join(argv)): argv = [sys.executable, "-Wa", "-m", "copyparty"] + argv sp.check_call(argv) else: sys.path.insert(0, os.getcwd()) from copyparty.__main__ import main as copyparty try: copyparty(["a"] + argv) except SystemExit as ex: if ex.code: raise print("\n\033[32mokke\033[0m") sys.exit(1) ================================================ FILE: .vscode/settings.json ================================================ { "workbench.colorCustomizations": { // https://ocv.me/dot/bifrost.html "terminal.background": "#1e1e1e", "terminal.foreground": "#d2d2d2", "terminalCursor.background": "#93A1A1", "terminalCursor.foreground": "#93A1A1", "terminal.ansiBlack": "#404040", "terminal.ansiRed": "#f03669", "terminal.ansiGreen": "#b8e346", "terminal.ansiYellow": "#ffa402", "terminal.ansiBlue": "#02a2ff", "terminal.ansiMagenta": "#f65be3", "terminal.ansiCyan": "#3da698", "terminal.ansiWhite": "#d2d2d2", "terminal.ansiBrightBlack": "#606060", "terminal.ansiBrightRed": "#c75b79", "terminal.ansiBrightGreen": "#c8e37e", "terminal.ansiBrightYellow": "#ffbe4a", "terminal.ansiBrightBlue": "#71cbff", "terminal.ansiBrightMagenta": "#b67fe3", "terminal.ansiBrightCyan": "#9cf0ed", "terminal.ansiBrightWhite": "#ffffff", }, "python.terminal.activateEnvironment": false, "python.analysis.enablePytestSupport": false, "python.analysis.typeCheckingMode": "standard", "python.testing.pytestEnabled": false, "python.testing.unittestEnabled": true, "python.testing.unittestArgs": [ "-v", "-s", "./tests", "-p", "test_*.py" ], // python3 -m isort --py=27 --profile=black ~/dev/copyparty/{copyparty,tests}/*.py && python3 -m black -t py27 ~/dev/copyparty/{copyparty,tests,bin}/*.py $(find ~/dev/copyparty/copyparty/stolen -iname '*.py') "editor.formatOnSave": false, "[html]": { "editor.formatOnSave": false, "editor.autoIndent": "keep", }, "[css]": { "editor.formatOnSave": false, }, "files.associations": { "*.makefile": "makefile" }, } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "label": "pre", "command": "true;rm -rf inc/* inc/.hist/;mkdir -p inc;", "type": "shell" }, { "label": "no_dbg", "type": "shell", "command": "${config:python.pythonPath}", "args": [ "-Wa", //-We ".vscode/launch.py" ] } ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ in the words of Abraham Lincoln: > Be excellent to each other... and... PARTY ON, DUDES! more specifically I'll paraphrase some examples from a german automotive corporation as they cover all the bases without being too wordy ## Examples of unacceptable behavior * intimidation, harassment, trolling * insulting, derogatory, harmful or prejudicial comments * posting private information without permission * political or personal attacks ## Examples of expected behavior * being nice, friendly, welcoming, inclusive, mindful and empathetic * acting considerate, modest, respectful * using polite and inclusive language * criticize constructively and accept constructive criticism * respect different points of view ## finally and even more specifically, * parse opinions and feedback objectively without prejudice * it's the message that matters, not who said it aaand that's how you say `be nice` in a way that fills half a floppy w ================================================ FILE: CONTRIBUTING.md ================================================ * **found a bug?** [create an issue!](https://github.com/9001/copyparty/issues) or let me know in the [discord](https://discord.gg/25J8CdTT6G) :> * **fixed a bug?** create a PR or post a patch! big thx in advance :> * **have a cool idea?** let's discuss it! anywhere's fine, you choose. but please: # do not use AI / LLM when writing code copyparty is 100% organic, free-range, human-written software! > ⚠ you are now entering a no-copilot zone the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate. sorry for the harsh tone, but this is important to me 🙏 # contribution ideas ## documentation I think we can agree that the documentation leaves a LOT to be desired. I've realized I'm not exactly qualified for this 😅 but maybe the [soon-to-come setup GUI](https://github.com/9001/copyparty/issues/57) will make this more manageable. The best documentation is the one that never had to be written, right? :> so I suppose we can give this a wait-and-see approach for a bit longer. ## crazy ideas & features assuming they won't cause too much problems or side-effects :> i think someone was working on a way to list directories over DNS for example... if you wanna have a go at coding it up yourself then maybe mention the idea on discord before you get too far, otherwise just go nuts 👍 ## others aside from documentation and ideas, some other things that would be cool to have some help with is: * **translations** -- the copyparty web-UI has translations in [copyparty/web/tl](https://github.com/9001/copyparty/tree/hovudstraum/copyparty/web/tl); if you'd like to [add a translation](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :> * but please note that support for [RTL (Right-to-Left) languages](https://en.wikipedia.org/wiki/Right-to-left_script) is currently not planned, since the javascript is a bit too jank for that * **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive. * **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it! * **packaging** for various linux distributions -- this could either be as simple as just plopping the sfx.py in the right place and calling that from systemd (the archlinux package [originally did this](https://github.com/9001/copyparty/pull/18)); maybe with a small config-file which would cause copyparty to load settings from `/etc/copyparty.d` (like the [archlinux package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) does with `copyparty.conf`), or it could be a proper installation of the copyparty python package into /usr/lib or similar (the archlinux package [eventually went for this approach](https://github.com/9001/copyparty/pull/26)) * [fpm](https://github.com/jordansissel/fpm) can probably help with the technical part of it, but someone needs to handle distro relations :-) * **software integration** -- I'm sure there's a lot of usecases where copyparty could complement something else, or the other way around, so any ideas or any work in this regard would be dope. This doesn't necessarily have to be code inside copyparty itself; * [hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) -- these are small programs which are called by copyparty when certain things happen (files are uploaded, someone hits a 404, etc.), and could be a fun way to add support for more usecases * [parser plugins](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) -- if you want to have copyparty analyze and index metadata for some oddball file-formats, then additional plugins would be neat :> ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 ed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ### 💾🎉 copyparty turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser * server only needs Python (2 or 3), all dependencies optional * 🔌 protocols: [http(s)](#the-browser) // [webdav](#webdav-server) // [sftp](#sftp-server) // [ftp(s)](#ftp-server) // [tftp](#tftp-server) // [smb/cifs](#smb-server) * 📱 [android app](#android-app) // [iPhone shortcuts](#ios-shortcuts) 👉 **[Get started](#quickstart)!** or visit the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running on a nuc in my basement 📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer) 🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) // 👉 **[feature-showcase](https://a.ocv.me/pub/demo/showcase-hq.webm)** ([youtube](https://www.youtube.com/watch?v=15_-hgsX2V0)) built in Norway 🇳🇴 with contributions from [not-norway](https://github.com/9001/copyparty/graphs/contributors) ## readme toc * top * [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉 * [mirrors](#mirrors) - other places to download copyparty from * [at home](#at-home) - make it accessible over the internet * [on servers](#on-servers) - you may also want these, especially on servers * [features](#features) - also see [comparison to similar software](./docs/versus.md) * [testimonials](#testimonials) - small collection of user feedback * [motivations](#motivations) - project goals / philosophy * [notes](#notes) - general notes * [bugs](#bugs) - roughly sorted by chance of encounter * [not my bugs](#not-my-bugs) - same order here too * [breaking changes](#breaking-changes) - upgrade notes * [FAQ](#FAQ) - "frequently" asked questions * [accounts and volumes](#accounts-and-volumes) - per-folder, per-user permissions * [shadowing](#shadowing) - hiding specific subfolders * [dotfiles](#dotfiles) - unix-style hidden files/folders * [the browser](#the-browser) - accessing a copyparty server using a web-browser * [tabs](#tabs) - the main tabs in the ui * [hotkeys](#hotkeys) - the browser has the following hotkeys * [navpane](#navpane) - switching between breadcrumbs or navpane * [thumbnails](#thumbnails) - press `g` or `田` to toggle grid-view instead of the file listing * [zip downloads](#zip-downloads) - download folders (or file selections) as `zip` or `tar` files * [uploading](#uploading) - drag files/folders into the web-browser to upload * [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server * [unpost](#unpost) - undo/delete accidental uploads * [self-destruct](#self-destruct) - uploads can be given a lifetime * [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)) * [incoming files](#incoming-files) - the control-panel shows the ETA for all incoming files * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission) * [shares](#shares) - share a file or folder by creating a temporary link * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI * [rss feeds](#rss-feeds) - monitor a folder with your RSS reader * [opds feeds](#opds-feeds) - browse and download files from your e-book reader * [recent uploads](#recent-uploads) - list all recent uploads * [media player](#media-player) - plays almost every audio format there is * [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists * [creating a playlist](#creating-a-playlist) - with a standalone mediaplayer or copyparty * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression) * [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings * [textfile viewer](#textfile-viewer) - with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/)) * [markdown viewer](#markdown-viewer) - and there are *two* editors * [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion * [other tricks](#other-tricks) * [searching](#searching) - search by size, date, path/name, mp3-tags, ... * [server config](#server-config) - using arguments or config files, or a mix of both * [version-checker](#version-checker) - sleep better at night * [logging](#logging) - serverlog is sent to stdout by default * [zeroconf](#zeroconf) - announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png)) * [mdns](#mdns) - LAN domain-name and feature announcer * [ssdp](#ssdp) - windows-explorer announcer * [qr-code](#qr-code) - print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/194728533-6f00849b-c6ac-43c6-9359-83e454d11e00.png) for quick access * [ftp server](#ftp-server) - an FTP server can be started using `--ftp 3921` * [sftp server](#sftp-server) - goes roughly 700 MiB/s (slower than webdav and ftp) * [webdav server](#webdav-server) - with read-write support * [connecting to webdav from windows](#connecting-to-webdav-from-windows) - using the GUI * [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969` * [smb server](#smb-server) - unsafe, slow, not recommended for wan * [browser ux](#browser-ux) - tweaking the ui * [opengraph](#opengraph) - discord and social-media embeds * [file deduplication](#file-deduplication) - enable symlink-based upload deduplication * [file indexing](#file-indexing) - enable music search, upload-undo, and better dedup * [exclude-patterns](#exclude-patterns) - to save some time * [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems * [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 * [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes * [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags * [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/)) * [zeromq](#zeromq) - event-hooks can send zeromq messages * [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/)) * [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/)) * [ip auth](#ip-auth) - autologin based on IP range (CIDR) * [restrict to ip](#restrict-to-ip) - limit a user to certain IP ranges (CIDR) * [identity providers](#identity-providers) - replace copyparty passwords with oauth and such * [generic header auth](#generic-header-auth) - other ways to auth by header * [user-changeable passwords](#user-changeable-passwords) - if permitted, users can change their own passwords * [using the cloud as storage](#using-the-cloud-as-storage) - connecting to an aws s3 bucket and similar * [hiding from google](#hiding-from-google) - tell search engines you don't wanna be indexed * [themes](#themes) * [complete examples](#complete-examples) * [listen on port 80 and 443](#listen-on-port-80-and-443) - become a *real* webserver * [reverse-proxy](#reverse-proxy) - running copyparty next to other websites * [real-ip](#real-ip) - teaching copyparty how to see client IPs * [reverse-proxy performance](#reverse-proxy-performance) * [permanent cloudflare tunnel](#permanent-cloudflare-tunnel) - if you have a domain and want to get your copyparty online real quick * [prometheus](#prometheus) - metrics/stats can be enabled * [other extremely specific features](#other-extremely-specific-features) - you'll never find a use for these * [custom mimetypes](#custom-mimetypes) - change the association of a file extension * [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally... * [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out * [feature beefybits](#feature-beefybits) - force-enable features with known issues on your OS/env * [packages](#packages) - the party might be closer than you think * [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/)) * [fedora package](#fedora-package) - does not exist yet * [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg` * [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nixos module](#nixos-module) * [browser support](#browser-support) - TLDR: yes * [server hall of fame](#server-hall-of-fame) - unexpected things that run copyparty * [client examples](#client-examples) - interact with copyparty using non-browser clients * [folder sync](#folder-sync) - sync folders to/from copyparty * [mount as drive](#mount-as-drive) - a remote copyparty server as a local filesystem * [android app](#android-app) - upload to copyparty with one tap * [iOS shortcuts](#iOS-shortcuts) - there is no iPhone app, but * [performance](#performance) - defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload * [client-side](#client-side) - when uploading files * [security](#security) - there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements * [gotchas](#gotchas) - behavior that might be unexpected * [cors](#cors) - cross-site request config * [filekeys](#filekeys) - prevent filename bruteforcing * [dirkeys](#dirkeys) - share specific folders in a volume * [password hashing](#password-hashing) - you can hash passwords * [https](#https) - both HTTP and HTTPS are accepted * [recovering from crashes](#recovering-from-crashes) * [client crashes](#client-crashes) * [firefox wsod](#firefox-wsod) - firefox 87 can crash during uploads * [HTTP API](#HTTP-API) - see [devnotes](./docs/devnotes.md#http-api) * [dependencies](#dependencies) - mandatory deps * [optional dependencies](#optional-dependencies) - enable bonus features * [dependency chickenbits](#dependency-chickenbits) - prevent loading an optional dependency * [dependency unvendoring](#dependency-unvendoring) - force use of system modules * [optional gpl stuff](#optional-gpl-stuff) * [sfx](#sfx) - the self-contained "binary" (recommended!) * [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+) * [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) * [install on android](#install-on-android) * [install on iOS](#install-on-iOS) * [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them * [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md) ## quickstart just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉 > ℹ️ the sfx is a [self-extractor](https://github.com/9001/copyparty/issues/270) which unpacks an embedded `tar.gz` into `$TEMP` -- if this looks too scary, you can use the [zipapp](#zipapp) which has slightly worse performance * or install through [pypi](https://pypi.org/project/copyparty/): `python3 -m pip install --user -U copyparty` * or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead * or install [on arch](#arch-package) / [homebrew](#homebrew-formulae) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package) * or if you are on android, [install copyparty in termux](#install-on-android) * or maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS) * or maybe you have a [synology nas / dsm](./docs/synology-dsm.md) * or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty` * or if your computer is messed up and nothing else works, [try the pyz](#zipapp) * or if your OS is dead, give the [bootable flashdrive / cd-rom](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) a spin * or if you don't trust copyparty yet and want to isolate it a little, then... * ...maybe [prisonparty](./bin/prisonparty.sh) to create a tiny [chroot](https://wiki.archlinux.org/title/Chroot) (very portable), * ...or [bubbleparty](./bin/bubbleparty.sh) to wrap it in [bubblewrap](https://github.com/containers/bubblewrap) (much better) * or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too * docker has all deps built-in, so skip this step: enable thumbnails (images/audio/video), media indexing, and audio transcoding by installing some recommended deps: * **Alpine:** `apk add py3-pillow ffmpeg` * **Debian:** `apt install --no-install-recommends python3-pil ffmpeg` * **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing` * **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg` * **MacOS:** `port install py-Pillow ffmpeg` * **MacOS** (alternative): `brew install pillow ffmpeg` * **Windows:** `python -m pip install --user -U Pillow` * install [python](https://www.python.org/downloads/windows/) and [ffmpeg](#optional-dependencies) manually; do not use `winget` or `Microsoft Store` (it breaks $PATH) * copyparty.exe comes with `Pillow` and only needs [ffmpeg](#optional-dependencies) for mediatags/videothumbs * see [optional dependencies](#optional-dependencies) to enable even more features running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes) or see [some usage examples](#complete-examples) for inspiration, or the [complete windows example](./docs/examples/windows.md) some recommended options: * `-e2dsa` enables general [file indexing](#file-indexing) * `-e2ts` enables audio metadata indexing (needs either FFprobe or Mutagen) * `-v /mnt/music:/music:r:rw,foo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, and read-write for user `foo`, password `bar` * replace `:r:rw,foo` with `:r,foo` to only make the folder readable by `foo` and nobody else * see [accounts and volumes](#accounts-and-volumes) (or [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page)) for the syntax and other permissions ### mirrors other places to download copyparty from (non-github links): * https://copyparty.eu/ (hetzner, finland, official mirror): * https://copyparty.eu/py = https://copyparty.eu/copyparty-sfx.py = the sfx * https://copyparty.eu/en = https://copyparty.eu/copyparty-en.py = the english-only sfx * https://copyparty.eu/pyz = https://copyparty.eu/copyparty.pyz = the zipapp * https://copyparty.eu/enz = https://copyparty.eu/copyparty-en.pyz = the enterprise pyz * https://copyparty.eu/cli = online cli helptext ### at home make it accessible over the internet by starting a [cloudflare quicktunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) like so: first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) and then start the tunnel with `cloudflared tunnel --url http://127.0.0.1:3923` as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you but if you have a domain, then you probably want to skip the random autogenerated URL and instead make a [permanent cloudflare tunnel](#permanent-cloudflare-tunnel) since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly ### on servers you may also want these, especially on servers: * [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service (see guide inside) * [contrib/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security) * [contrib/podman-systemd/](contrib/podman-systemd/) to run copyparty in a Podman container as a systemd service (see guide inside) * [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo * [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD * [nixos module](#nixos-module) to run copyparty on NixOS hosts * [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to [reverse-proxy](#reverse-proxy) behind nginx (for better https) and remember to open the ports you want; here's a complete example including every feature copyparty has to offer: ``` firewall-cmd --permanent --add-port={80,443,3921,3922,3923,3945,3990}/tcp # --zone=libvirt firewall-cmd --permanent --add-port=12000-12099/tcp # --zone=libvirt firewall-cmd --permanent --add-port={69,1900,3969,5353}/udp # --zone=libvirt firewall-cmd --reload ``` (69:tftp, 1900:ssdp, 3921:ftp, 3922:sftp, 3923:http/https, 3945:smb, 3969:tftp, 3990:ftps, 5353:mdns, 12000:passive-ftp) ## features also see [comparison to similar software](./docs/versus.md) * backend stuff * ☑ IPv6 + unix-sockets * ☑ [multiprocessing](#performance) (actual multithreading) * ☑ volumes (mountpoints) * ☑ [accounts](#accounts-and-volumes) * ☑ [ftp server](#ftp-server) * ☑ [tftp server](#tftp-server) * ☑ [webdav server](#webdav-server) * ☑ [smb/cifs server](#smb-server) * ☑ [qr-code](#qr-code) for quick access * ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf) * ☑ [event hooks](#event-hooks) / script runner * ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy) * ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64, SGI IRIX) * upload * ☑ basic: plain multipart, ie6 support * ☑ [up2k](#uploading): js, resumable, multithreaded * **no filesize limit!** even on Cloudflare * ☑ stash: simple PUT filedropper * ☑ filename randomizer * ☑ write-only folders * ☑ [unpost](#unpost): undo/delete accidental uploads * ☑ [self-destruct](#self-destruct) (specified server-side or client-side) * ☑ [race the beam](#race-the-beam) (almost like peer-to-peer) * ☑ symlink/discard duplicates (content-matching) * download * ☑ single files in browser * ☑ [folders as zip / tar files](#zip-downloads) * ☑ [FUSE client](https://github.com/9001/copyparty/tree/hovudstraum/bin#partyfusepy) (read-only) * browser * ☑ [navpane](#navpane) (directory tree sidebar) * ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename)) * ☑ audio player (with [OS media controls](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) and opus/mp3 transcoding) * ☑ play video files as audio (converted on server) * ☑ create and play [m3u8 playlists](#playlists) * ☑ image gallery with webm player * ☑ and cbz manga/comics reader * ☑ [textfile browser](#textfile-viewer) with syntax highlighting * ☑ realtime streaming of growing files (logfiles and such) * ☑ [thumbnails](#thumbnails) * ☑ ...of images using Pillow, pyvips, or FFmpeg * ☑ ...of RAW images using rawpy * ☑ ...of videos using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg * ☑ cache eviction (max-age; maybe max-size eventually) * ☑ multilingual UI (english, norwegian, chinese, [add your own](./docs/rice/#translations))) * ☑ SPA (browse while uploading) * server indexing * ☑ [locate files by contents](#file-search) * ☑ search by name/path/date/size * ☑ [search by ID3-tags etc.](#searching) * client support * ☑ [folder sync](#folder-sync) (one-way only; full sync will never be supported) * ☑ [curl-friendly](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) * ☑ [opengraph](#opengraph) (discord embeds) * markdown * ☑ [viewer](#markdown-viewer) * ☑ editor (sure why not) * ☑ [variables](#markdown-vars) PS: something missing? post any crazy ideas you've got as a [feature request](https://github.com/9001/copyparty/issues/new?assignees=9001&labels=enhancement&template=feature_request.md) or [discussion](https://github.com/9001/copyparty/discussions/new?category=ideas) 🤙 ## testimonials small collection of user feedback `good enough`, `surprisingly correct`, `certified good software`, `just works`, `why`, `wow this is better than nextcloud` * UI просто ужасно. Если буду описывать детально не смогу удержаться в рамках приличий # motivations project goals / philosophy * inverse unix philosophy -- do all the things, and do an *okay* job * quick drop-in service to get a lot of features in a pinch * some of [the alternatives](./docs/versus.md) might be a better fit for you * run anywhere, support everything * as many web-browsers and python versions as possible * every browser should at least be able to browse, download, upload files * be a good emergency solution for transferring stuff between ancient boxes * minimal dependencies * but optional dependencies adding bonus-features are ok * everything being plaintext makes it possible to proofread for malicious code * no preparations / setup necessary, just run the sfx (which is also plaintext) * adaptable, malleable, hackable * no build steps; modify the js/python without needing node.js or anything like that becoming rich is specifically *not* a motivation, but if you wanna donate then see my [github profile](https://github.com/9001) regarding donations for my FOSS stuff in general (also THANKS!) ## notes general notes: * paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale * because no browsers currently implement the media-query to do this properly orz browser-specific: * iPhone/iPad: use Firefox to download files * Android-Chrome: increase "parallel uploads" for higher speed (android bug) * Android-Firefox: takes a while to select files (their fix for ☝️) * Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now* * Desktop-Firefox: [may stop you from unplugging USB flashdrives](https://bugzilla.mozilla.org/show_bug.cgi?id=1792598) until you visit `about:memory` and click `Minimize memory usage` server-os-specific: * RHEL8 / Rocky8: you can run copyparty using `/usr/libexec/platform-python` server notes: * pypy is supported but regular cpython is faster if you enable the database # bugs roughly sorted by chance of encounter * general: * `--th-ff-jpg` may fix video thumbnails on some FFmpeg versions (macos, some linux) * `--th-ff-swr` may fix audio thumbnails on some FFmpeg versions * if the `up2k.db` (filesystem index) is on a samba-share or network disk, you'll get unpredictable behavior if the share is disconnected for a bit * use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db and thumbnails on a local disk instead * or, if you only want to move the db (and not the thumbnails), then use `--dbpath` or the `dbpath` volflag * all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise * probably more, pls let me know * python 3.4 and older (including 2.7): * many rare and exciting edge-cases because [python didn't handle EINTR yet](https://peps.python.org/pep-0475/) * downloads from copyparty may suddenly fail, but uploads *should* be fine * python 2.7 on Windows: * cannot index non-ascii filenames with `-e2d` * cannot handle filenames with mojibake if you have a new exciting bug to share, see [reporting bugs](#reporting-bugs) ## not my bugs same order here too * [Chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) -- if you try to upload a folder which contains symlinks by dragging it into the browser, the symlinked files will not get uploaded * [Chrome issue 1352210](https://bugs.chromium.org/p/chromium/issues/detail?id=1352210) -- plaintext http may be faster at filehashing than https (but also extremely CPU-intensive) * [Chrome issue 383568268](https://issues.chromium.org/issues/383568268) -- filereaders in webworkers can OOM / crash the browser-tab * copyparty has a workaround which seems to work well enough * [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files * Windows: Uploading from a webbrowser may fail with "directory iterator got stuck" due to the [max path length](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry); try moving the files somewhere shorter before uploading * Android: music playback randomly stops due to [battery usage settings](#fix-unreliable-playback-on-android) * iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11) * `AudioContext` will probably never be a viable workaround as apple introduces new issues faster than they fix current ones * iPhones: music volume goes on a rollercoaster during song changes * nothing I can do about it because `AudioContext` is still broken in safari * iPhones: the preload feature (in the media-player-options tab) can cause a tiny audio glitch 20sec before the end of each song, but disabling it may cause worse iOS bugs to appear instead * just a hunch, but disabling preloading may cause playback to stop entirely, or possibly mess with bluetooth speakers * tried to add a tooltip regarding this but looks like apple broke my tooltips * iPhones: preloaded awo files make safari log MEDIA_ERR_NETWORK errors as playback starts, but the song plays just fine so eh whatever * awo, opus-weba, is apple's new take on opus support, replacing opus-caf which was technically limited to cbr opus * iPhones: preloading another awo file may cause playback to stop * can be somewhat mitigated with `mp.au.play()` in `mp.onpreload` but that can hit a race condition in safari that starts playing the same audio object twice in parallel... * Windows: folders cannot be accessed if the name ends with `.` * python or windows bug * Windows: msys2-python 3.8.6 occasionally throws `RuntimeError: release unlocked lock` when leaving a scoped mutex in up2k * this is an msys2 bug, the regular windows edition of python is fine * VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf * use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db and thumbnails inside the vm instead * or, if you only want to move the db (and not the thumbnails), then use `--dbpath` or the `dbpath` volflag * also happens on mergerfs, so put the db elsewhere * Ubuntu: dragging files from certain folders into firefox or chrome is impossible * due to snap security policies -- see `snap connections firefox` for the allowlist, `removable-media` permits all of `/mnt` and `/media` apparently # breaking changes upgrade notes * `1.9.16` (2023-11-04): * `--stats`/prometheus: `cpp_bans` renamed to `cpp_active_bans`, and that + `cpp_uptime` are gauges * `1.6.0` (2023-01-29): * http-api: delete/move is now `POST` instead of `GET` * everything other than `GET` and `HEAD` must pass [cors validation](#cors) * `1.5.0` (2022-12-03): [new chunksize formula](https://github.com/9001/copyparty/commit/54e1c8d261df) for files larger than 128 GiB * **users:** upgrade to the latest [cli uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) if you use that * **devs:** update third-party up2k clients (if those even exist) # FAQ "frequently" asked questions * CopyParty? * nope! the name is either copyparty (all-lowercase) or Copyparty -- it's [one word](https://en.wiktionary.org/wiki/copyparty) after all :> * what is a volflag? * per-volume configuration; many (not all) global-options can be set as volflags, and most (not all) volflags can be set as global-options; [complete list of volflags](https://copyparty.eu/cli/#flags-help-page) * what is a volume? * a mapping from a URL (`/music/`) to a folder on your server's local filesystem (`C:\Users\ed\Music`) which can then be accessed through copyparty, depending on the permissions and options you set on it -- see [accounts and volumes](#accounts-and-volumes) * can I change the 🌲 spinning pine-tree loading animation? * [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-( * is it possible to block read-access to folders unless you know the exact URL for a particular file inside? * yes, using the [`g` permission](#accounts-and-volumes), see the examples there * you can also do this with linux filesystem permissions; `chmod 111 music` will make it possible to access files and folders inside the `music` folder but not list the immediate contents -- also works with other software, not just copyparty * can I link someone to a password-protected volume/file by including the password in the URL? * yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end * if you have enabled `--usernames` then do `?pw=username:password` instead * `?pw` can be disabled with `--pw-urlp=A` but this breaks support for many clients * how do I stop `.hist` folders from appearing everywhere on my HDD? * by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location) * can I make copyparty download a file to my server if I give it a URL? * yes, using [hooks](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py) * firefox refuses to connect over https, saying "Secure Connection Failed" or "SEC_ERROR_BAD_SIGNATURE", but the usual button to "Accept the Risk and Continue" is not shown * firefox has corrupted its certstore; fix this by exiting firefox, then find and delete the file named `cert9.db` somewhere in your firefox profile folder * the server keeps saying `thank you for playing` when I try to access the website * you've gotten banned for malicious traffic! if this happens by mistake, and you're running a reverse-proxy and/or something like cloudflare, see [real-ip](#real-ip) on how to fix this * copyparty seems to think I am using http, even though the URL is https * your reverse-proxy is not sending the `X-Forwarded-Proto: https` header; this could be because your reverse-proxy itself is confused. Ensure that none of the intermediates (such as cloudflare) are terminating https before the traffic hits your entrypoint * thumbnails are broken (you get a colorful square which says the filetype instead) * you need to install `FFmpeg` or `Pillow`; see [thumbnails](#thumbnails) * thumbnails are broken, specifically for photos and videos taken by iphones * the [docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker) and [bootable flashdrive](https://a.ocv.me/pub/stuff/edcd001/enterprise-edition/) are not able to read heif/heic images and h265/HEVC video due to [legal reasons](docs/bad-codecs.md) * thumbnails are broken (some images appear, but other files just get a blank box, and/or the broken-image placeholder) * probably due to a reverse-proxy messing with the request URLs and stripping the query parameters (`?th=w`), so check your URL rewrite rules * could also be due to incorrect caching settings in reverse-proxies and/or CDNs, so make sure that nothing is set to ignore the query string * could also be due to misbehaving privacy-related browser extensions, so try to disable those * i want to learn python and/or programming and am considering looking at the copyparty source code in that occasion * ```bash _| _ __ _ _|_ (_| (_) | | (_) |_ ``` # accounts and volumes per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments * much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if the user has `a`/admin in any volume) * changes to the `[global]` config section requires a restart to take effect a quick summary can be seen using [`--help-accounts`](https://copyparty.eu/cli/#accounts-help-page) configuring accounts/volumes with arguments: * `-a usr:pwd` adds account `usr` with password `pwd` * `-v .::r` adds current-folder `.` as the webroot, `r`eadable by anyone * the syntax is `-v src:dst:perm:perm:...` so local-path, url-path, and one or more permissions to set * granting the same permissions to multiple accounts: `-v .::r,usr1,usr2:rw,usr3,usr4` = usr1/2 read-only, 3/4 read-write permissions: * `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys * `w` (write): upload files, move/copy files *into* this folder * `m` (move): move files/folders *from* this folder * `d` (delete): delete files/folders * `.` (dots): user can ask to show dotfiles in directory listings * `g` (get): only download files, cannot see folder contents or zip/tar * `G` (upget): same as `g` except uploaders get to see their own [filekeys](#filekeys) (see `fk` in examples below) * `h` (html): same as `g` except folders return their index.html, and filekeys are not necessary for index.html * `a` (admin): can see upload time, uploader IPs, config-reload * `A` ("all"): same as `rwmda.` (read/write/move/delete/admin/dotfiles) examples: * add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3` * make folder `/srv` the root of the filesystem, read-only by anyone: `-v /srv::r` * make folder `/mnt/music` available at `/music`, read-only for u1 and u2, read-write for u3: `-v /mnt/music:music:r,u1,u2:rw,u3` * unauthorized users accessing the webroot can see that the `music` folder exists, but cannot open it * make folder `/mnt/incoming` available at `/inc`, write-only for u1, read-move for u2: `-v /mnt/incoming:inc:w,u1:rm,u2` * unauthorized users accessing the webroot can see that the `inc` folder exists, but cannot open it * `u1` can open the `inc` folder, but cannot see the contents, only upload new files to it * `u2` can browse it and move files *from* `/inc` into any folder where `u2` has write-access * make folder `/mnt/ss` available at `/i`, read-write for u1, get-only for everyone else, and enable filekeys: `-v /mnt/ss:i:rw,u1:g:c,fk=4` * `c,fk=4` sets the `fk` ([filekey](#filekeys)) volflag to 4, meaning each file gets a 4-character accesskey * `u1` can upload files, browse the folder, and see the generated filekeys * other users cannot browse the folder, but can access the files if they have the full file URL with the filekey * replacing the `g` permission with `wg` would let anonymous users upload files, but not see the required filekey to access it * replacing the `g` permission with `wG` would let anonymous users upload files, receiving a working direct link in return if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct` * to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct` * single users can also be subtracted from a group: `@admins,-james` anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf` * you can also `PRTY_CONFIG=foobar.conf python copyparty-sfx.py` (convenient in docker etc) ```yaml [accounts] u1: p1 # create account "u1" with password "p1" u2: p2 # (note that comments must have u3: p3 # two spaces before the # sign) [groups] g1: u1, u2 # create a group [/] # this URL will be mapped to... /srv # ...this folder on the server filesystem accs: r: * # read-only for everyone, no account necessary [/music] # create another volume at this URL, /mnt/music # which is mapped to this folder accs: r: u1, u2 # only these accounts can read, r: @g1 # (exactly the same, just with a group instead) r: @acct # (alternatively, ALL users who are logged in) rw: u3 # and only u3 can read-write [/inc] /mnt/incoming accs: w: u1 # u1 can upload but not see/download any files, rm: u2 # u2 can browse + move files out of this volume [/i] /mnt/ss accs: rw: u1 # u1 can read-write, g: * # everyone can access files if they know the URL flags: fk: 4 # each file URL will have a 4-character password ``` ## shadowing hiding specific subfolders by mounting another volume on top of them for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty` the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead > ℹ️ this also works for single files, because files can also be volumes ## dotfiles unix-style hidden files/folders by starting the name with a dot anyone can access these if they know the name, but they normally don't appear in directory listings a client can request to see dotfiles in directory listings if global option `-ed` is specified, or the volume has volflag `dots`, or the user has permission `.` dotfiles do not appear in search results unless one of the above is true, **and** the global option / volflag `dotsrch` is set > even if user has permission to see dotfiles, they are default-hidden unless `--see-dots` is set, and/or user has enabled the `dotfiles` option in the settings tab config file example, where the same permission to see dotfiles is given in two different ways just for reference: ```yaml [/foo] /srv/foo accs: r.: ed # user "ed" has read-access + dot-access in this volume; # dotfiles are visible in listings, but not in searches flags: dotsrch # dotfiles will now appear in search results too dots # another way to let everyone see dotfiles in this vol ``` # the browser accessing a copyparty server using a web-browser ![copyparty-browser-fs8](https://user-images.githubusercontent.com/241032/192042695-522b3ec7-6845-494a-abdb-d1c0d0e23801.png) ## tabs the main tabs in the ui * `[🔎]` [search](#searching) by size, date, path/name, mp3-tags ... * `[🧯]` [unpost](#unpost): undo/delete accidental uploads * `[🚀]` and `[🎈]` are the [uploaders](#uploading) * `[📂]` mkdir: create directories * `[📝]` new-file: create a new textfile * `[📟]` send-msg: either to server-log or into textfiles if `--urlform save` * `[🎺]` audio-player config options * `[⚙️]` general client config options ## hotkeys the browser has the following hotkeys (always qwerty) * `?` show hotkeys help * `B` toggle breadcrumbs / [navpane](#navpane) * `I/K` prev/next folder * `M` parent folder (or unexpand current) * `V` toggle folders / textfiles in the navpane * `G` toggle list / [grid view](#thumbnails) -- same as `田` bottom-right * `T` toggle thumbnails / icons * `ESC` close various things * `ctrl-K` delete selected files/folders * `ctrl-X` cut selected files/folders * `ctrl-C` copy selected files/folders to clipboard * `ctrl-V` paste (move/copy) * `Y` download selected files * `F2` [rename](#batch-rename) selected file/folder * when a file/folder is selected (in not-grid-view): * `Up/Down` move cursor * shift+`Up/Down` select and move cursor * ctrl+`Up/Down` move cursor and scroll viewport * `Space` toggle file selection * `Ctrl-A` toggle select all * when a textfile is open: * `I/K` prev/next textfile * `S` toggle selection of open file * `M` close textfile * when playing audio: * `J/L` prev/next song * `U/O` skip 10sec back/forward * `0..9` jump to 0%..90% * `P` play/pause (also starts playing the folder) * `Y` download file * when viewing images / playing videos: * `J/L, Left/Right` prev/next file * `Home/End` first/last file * `F` toggle fullscreen * `S` toggle selection * `R` rotate clockwise (shift=ccw) * `Y` download file * `Esc` close viewer * videos: * `U/O` skip 10sec back/forward * `0..9` jump to 0%..90% * `P/K/Space` play/pause * `M` mute * `C` continue playing next video * `V` loop entire file * `[` loop range (start) * `]` loop range (end) * when the navpane is open: * `A/D` adjust tree width * in the [grid view](#thumbnails): * `S` toggle multiselect * shift+`A/D` zoom * in the markdown editor: * `^s` save * `^h` header * `^k` autoformat table * `^u` jump to next unicode character * `^e` toggle editor / preview * `^up, ^down` jump paragraphs ## navpane switching between breadcrumbs or navpane click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (default), or a navpane (tree-browser sidebar thing) * `[+]` and `[-]` (or hotkeys `A`/`D`) adjust the size * `[🎯]` jumps to the currently open folder * `[📃]` toggles between showing folders and textfiles * `[📌]` shows the name of all parent folders in a docked panel * `[a]` toggles automatic widening as you go deeper * `[↵]` toggles wordwrap * `[👀]` show full name on hover (if wordwrap is off) ## thumbnails press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails * can be made default globally with `--grid` or per-volume with volflag `grid` * enable by adding `?imgs` to a link, or disable with `?imgs=0` ![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png) it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are * pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg * disable thumbnails for specific volumes with volflag `dthumb` for all, or `dvthumb` / `dathumb` / `dithumb` for video/audio/images only * for installing FFmpeg on windows, see [optional dependencies](#optional-dependencies) audio files are converted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`) images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg` * the order is significant, so if both `cover.png` and `folder.jpg` exist in a folder, it will pick the first matching `--th-covers` entry (`folder.jpg`) * and, if you enable [file indexing](#file-indexing), it will also try those names as dotfiles (`.folder.jpg` and so), and then fallback on the first picture in the folder (if it has any pictures at all) enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select * `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking * the `sel` option can be made default globally with `--gsel` or per-volume with volflag `gsel` to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` and `.elf` files respectively, do this: `--ext-th=exe=/icons/exe.png --ext-th=elf=/icons/elf.gif` * optionally as separate volflags for each mapping; see config file example below * the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types) * be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however note: * heif/heifs/heic/heics images usually require the `libvips` [optional dependency](#optional-dependencies) but this is not possible with the docker-images due to [legal reasons](docs/bad-codecs.md) config file example: ```yaml [global] no-thumb # disable ALL thumbnails and audio transcoding no-vthumb # only disable video thumbnails [/music] /mnt/nas/music accs: r: * # everyone can read flags: dthumb # disable ALL thumbnails and audio transcoding dvthumb # only disable video thumbnails ext-th: exe=/ico/exe.png # /ico/exe.png is the thumbnail of *.exe ext-th: elf=/ico/elf.gif # ...and /ico/elf.gif is used for *.elf th-covers: folder.png,folder.jpg,cover.png,cover.jpg # the default ``` ## zip downloads download folders (or file selections) as `zip` or `tar` files select which type of archive you want in the `[⚙️] config` tab: | name | url-suffix | description | |--|--|--| | `tar` | `?tar` | plain gnutar, works great with `curl \| tar -xv` | | `pax` | `?tar=pax` | pax-format tar, futureproof, not as fast | | `tgz` | `?tar=gz` | gzip compressed gnu-tar (slow), for `curl \| tar -xvz` | | `txz` | `?tar=xz` | gnu-tar with xz / lzma compression (v.slow) | | `zip` | `?zip` | works everywhere, glitchy filenames on win7 and older | | `zip_dos` | `?zip=dos` | traditional cp437 (no unicode) to fix glitchy filenames | | `zip_crc` | `?zip=crc` | cp437 with crc32 computed early for truly ancient software | * gzip default level is `3` (0=fast, 9=best), change with `?tar=gz:9` * xz default level is `1` (0=fast, 9=best), change with `?tar=xz:9` * bz2 default level is `2` (1=fast, 9=best), change with `?tar=bz2:9` * hidden files ([dotfiles](#dotfiles)) are excluded unless account is allowed to list them * `up2k.db` and `dir.txt` is always excluded * bsdtar supports streaming unzipping: `curl foo?zip | bsdtar -xv` * good, because copyparty's zip is faster than tar on small files * but `?tar` is better for large files, especially if the total exceeds 4 GiB * `zip_crc` will take longer to download since the server has to read each file twice * this is only to support MS-DOS PKZIP v2.04g (october 1993) and older * how are you accessing copyparty actually you can also zip a selection of files or folders by clicking them in the browser, that brings up a selection editor and zip button in the bottom right ![copyparty-zipsel-fs8](https://user-images.githubusercontent.com/241032/129635374-e5136e01-470a-49b1-a762-848e8a4c9cdc.png) cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive * super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways * and url-param `&nodot` skips dotfiles/dotfolders; they are included by default if your account has permission to see them * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms) * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0` ## uploading drag files/folders into the web-browser to upload dragdrop is the recommended way, but you may also: * select some files (not folders) in your file explorer and press CTRL-V inside the browser window * use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) * upload using [curl, sharex, ishare, ...](#client-examples) when uploading files through dragdrop or CTRL-V, this initiates an upload using `up2k`; there are two browser-based uploaders available: * `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0 * `[🚀] up2k`, the good / fancy one NB: you can undo/delete your own uploads with `[🧯]` [unpost](#unpost) (and this is also where you abort unfinished uploads, but you have to refresh the page first) up2k has several advantages: * you can drop folders into the browser (files are added recursively) * files are processed in chunks, and each chunk is checksummed * uploads autoresume if they are interrupted by network issues * uploads resume if you reboot your browser or pc, just upload the same files again * server detects any corruption; the client reuploads affected chunks * the client doesn't upload anything that already exists on the server * no filesize limit, even when a proxy limits the request size (for example Cloudflare) * much higher speeds than ftp/scp/tarpipe on some internet connections (mainly american ones) thanks to parallel connections * the last-modified timestamp of the file is preserved > it is perfectly safe to restart / upgrade copyparty while someone is uploading to it! > all known up2k clients will resume just fine 💪 see [up2k](./docs/devnotes.md#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d) ![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.png) **protip:** you can avoid scaring away users with [contrib/plugins/minimal-up2k.js](contrib/plugins/minimal-up2k.js) which makes it look [much simpler](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) **protip:** if you enable `favicon` in the `[⚙️] settings` tab (by typing something into the textbox), the icon in the browser tab will indicate upload progress -- also, the `[🔔]` and/or `[🔊]` switches enable visible and/or audible notifications on upload completion the up2k UI is the epitome of polished intuitive experiences: * "parallel uploads" specifies how many chunks to upload at the same time * `[🏃]` analysis of other files should continue while one is uploading * `[🥔]` shows a simpler UI for faster uploads from slow devices * `[🛡️]` decides when to overwrite existing files on the server * `🛡️` = never (generate a new filename instead) * `🕒` = overwrite if the server-file is older * `♻️` = always overwrite if the files are different * `[🎲]` generate random filenames during upload * `[🔎]` switch between upload and [file-search](#file-search) mode * ignore `[🔎]` if you add files by dragging them into the browser and then there's the tabs below it, * `[ok]` is the files which completed successfully * `[ng]` is the ones that failed / got rejected (already exists, ...) * `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order * `[busy]` files which are currently hashing, pending-upload, or uploading * plus up to 3 entries each from `[done]` and `[que]` for context * `[que]` is all the files that are still queued note that since up2k has to read each file twice, `[🎈] bup` can *theoretically* be up to 2x faster in some extreme cases (files bigger than your ram, combined with an internet connection faster than the read-speed of your HDD, or if you're uploading from a cuo2duo) if you are resuming a massive upload and want to skip hashing the files which already finished, you can enable `turbo` in the `[⚙️] config` tab, but please read the tooltip on that button if the server is behind a proxy which imposes a request-size limit, you can configure up2k to sneak below the limit with server-option `--u2sz` (the default is 96 MiB to support Cloudflare) if you want to replace existing files on the server with new uploads by default, run with `--u2ow 2` (only works if users have the delete-permission, and can still be disabled with `🛡️` in the UI) ### file-search dropping files into the browser also lets you see if they exist on the server ![copyparty-fsearch-fs8](https://user-images.githubusercontent.com/241032/129635361-c79286f0-b8f1-440e-aaf4-6e929428fac9.png) when you drag/drop files into the browser, you will see two dropzones: `Upload` and `Search` > on a phone? toggle the `[🔎]` switch green before tapping the big yellow Search button to select your files the files will be hashed on the client-side, and each hash is sent to the server, which checks if that file exists somewhere files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]` * the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much if you have a "wark" (file-identifier/checksum) then you can also search for that in the [🔎] tab by putting `w = kFpDiztbZc8Z1Lzi` in the `raw` field ### unpost undo/delete accidental uploads using the `[🧯]` tab in the UI ![copyparty-unpost-fs8](https://user-images.githubusercontent.com/241032/129635368-3afa6634-c20f-418c-90dc-ec411f3b3897.png) you can unpost even if you don't have regular move/delete access, however only for files uploaded within the past `--unpost` seconds (default 12 hours) and the server must be running with `-e2d` config file example: ```yaml [global] e2d # enable up2k database (remember uploads) unpost: 43200 # 12 hours (default) ``` ### self-destruct uploads can be given a lifetime, after which they expire / self-destruct the feature must be enabled per-volume with the `lifetime` [upload rule](#upload-rules) which sets the upper limit for how long a file gets to stay on the server clients can specify a shorter expiration time using the [up2k ui](#uploading) -- the relevant options become visible upon navigating into a folder with `lifetimes` enabled -- or by using the `life` [upload modifier](./docs/devnotes.md#write) specifying a custom expiration time client-side will affect the timespan in which unposts are permitted, so keep an eye on the estimates in the up2k ui ### race the beam download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)) -- it's almost like peer-to-peer requires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program ### incoming files the control-panel shows the ETA for all incoming files , but only for files being uploaded into volumes where you have read-access ![copyparty-cpanel-upload-eta-or8](https://github.com/user-attachments/assets/fd275ffa-698c-4fca-a307-4d2181269a6a) ## file manager cut/paste, rename, and delete files/folders (if you have permission) file selection: click somewhere on the line (not the link itself), then: * `space` to toggle * `up/down` to move * `shift-up/down` to move-and-select * `ctrl-shift-up/down` to also scroll * shift-click another line for range-select * cut: select some files and `ctrl-x` * copy: select some files and `ctrl-c` * paste: `ctrl-v` in another folder * rename: `F2` you can copy/move files across browser tabs (cut/copy in one tab, paste in another) ## shares share a file or folder by creating a temporary link when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or alternatively: * select a folder first to share that folder instead * select one or more files to share only those files this feature was made with [identity providers](#identity-providers) in mind -- configure your reverseproxy to skip the IdP's access-control for a given URL prefix and use that to safely share specific files/folders sans the usual auth checks when creating a share, the creator can choose any of the following options: * password-protection * expire after a certain time; `0` or blank means infinite * allow visitors to upload (if the user who creates the share has write-access) semi-intentional limitations: * cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d` * only folders from the same volume are shared; if you are sharing a folder which contains other volumes, then the contents of those volumes will not be available * if you change [password hashing](#password-hashing) settings after creating a password-protected share, then that share will stop working * related to [IdP volumes being forgotten on shutdown](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#idp-volumes-are-forgotten-on-shutdown), any shares pointing into a user's IdP volume will be unavailable until that user makes their first request after a restart * no option to "delete after first access" because tricky * when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit * browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky) specify `--shr /foobar` to enable this feature; a toplevel virtual folder named `foobar` is then created, and that's where all the shares will be served from * you can name it whatever, `foobar` is just an example * if you're using config files, put `shr: /foobar` inside the `[global]` section instead users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server the volflag `--shr-who` lets you control who can create a share from that volume, either `no` (nobody), `a` (people with admin permission), or `auth` (people who are logged in) after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there **security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder. ## batch rename select some files and press `F2` to bring up the rename UI ![batch-rename-fs8](https://user-images.githubusercontent.com/241032/128434204-eb136680-3c07-4ec7-92e0-ae86af20c241.png) quick explanation of the buttons, * `[✅ apply rename]` confirms and begins renaming * `[❌ cancel]` aborts and closes the rename window * `[↺ reset]` reverts any filename changes back to the original name * `[decode]` does a URL-decode on the filename, fixing stuff like `&` and `%20` * `[advanced]` toggles advanced mode advanced mode: rename files based on rules to decide the new names, based on the original name (regex), or based on the tags collected from the file (artist/title/...), or a mix of both in advanced mode, * `[case]` toggles case-sensitive regex * `regex` is the regex pattern to apply to the original filename; any files which don't match will be skipped * `format` is the new filename, taking values from regex capturing groups and/or from file tags * very loosely based on foobar2000 syntax * `presets` lets you save rename rules for later available functions: * `$lpad(text, length, pad_char)` * `$rpad(text, length, pad_char)` two counters are available; `.n.s` is the nth file in the selection, and `.n.d` the nth file in the folder, for example rename-output `file(.n.d).(ext)` gives `file5.bin`, and `beach-$lpad((.n.s),3,0).(ext)` is `beach-017.jpg` and the initial value of each counter can be set in the textboxes underneath the preset dropdown so, say you have a file named [`meganeko - Eclipse - 07 Sirius A.mp3`](https://www.youtube.com/watch?v=-dtb0vDPruI) (absolutely fantastic album btw) and the tags are: `Album:Eclipse`, `Artist:meganeko`, `Title:Sirius A`, `tn:7` you could use just regex to rename it: * `regex` = `(.*) - (.*) - ([0-9]{2}) (.*)` * `format` = `(3). (1) - (4)` * `output` = `07. meganeko - Sirius A.mp3` or you could use just tags: * `format` = `$lpad((tn),2,0). (artist) - (title).(ext)` * `output` = `7. meganeko - Sirius A.mp3` or a mix of both: * `regex` = ` - ([0-9]{2}) ` * `format` = `(1). (artist) - (title).(ext)` * `output` = `07. meganeko - Sirius A.mp3` the metadata keys you can use in the format field are the ones in the file-browser table header (whatever is collected with `-mte` and `-mtp`) ## rss feeds monitor a folder with your RSS reader , optionally recursive must be enabled per-volume with volflag `rss` or globally with `--rss` the feed includes itunes metadata for use with podcast readers such as [AntennaPod](https://antennapod.org/) a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3 url parameters: * `pw=hunter2` for password auth * if you enabled `--usernames` then do `pw=username:password` instead * `nopw` disables embedding the password (if provided) into item-URLs in the feed * `nopw=a` disables mentioning the password anywhere at all in the feed; may break some readers * `recursive` to also include subfolders * `title=foo` changes the feed title (default: folder name) * `fext=mp3,opus` only include mp3 and opus files (default: all) * `nf=30` only show the first 30 results (default: 250) * `sort=m` sort by mtime (file last-modified), newest first (default) * `u` = upload-time; NOTE: non-uploaded files have upload-time `0` * `n` = filename * `a` = filesize * uppercase = reverse-sort; `M` = oldest file first ## opds feeds browse and download files from your e-book reader enabled with the `opds` volflag or `--opds` global option add `?opds` to the end of the url you would like to browse, then input that in your opds client. for example: `https://copyparty.example/books/?opds`. to log in with a password, enter it into either of the username or password fields in your client. - if you've enabled `--usernames`, then you need to enter both username and password . note: some clients (e.g. Moon+ Reader) will not send the password when downloading cover images, which will cause your ip to be banned by copyparty. to work around this, you can grant the [`g` permission](#accounts-and-volumes) to unauthenticated requests and enable [filekeys](#filekeys) to prevent guessing filenames. for example: `-vbooks:books:r,ed:g:c,fk,opds` by default, not all file types will be listed in opds feeds. to change this, add the extension to `--opds-exts` (volflag: `opds_exts`), or empty the list to list everything ## recent uploads list all recent uploads by clicking "show recent uploads" in the controlpanel will show uploader IP and upload-time if the visitor has the admin permission * global-option `--ups-when` makes upload-time visible to all users, and not just admins * global-option `--ups-who` (volflag `ups_who`) specifies who gets access (0=nobody, 1=admins, 2=everyone), default=2 note that the [🧯 unpost](#unpost) feature is better suited for viewing *your own* recent uploads, as it includes the option to undo/delete them config file example: ```yaml [global] ups-when # everyone can see upload times ups-who: 1 # but only admins can see the list, # so ups-when doesn't take effect ``` ## media player plays almost every audio format there is (if the server has FFmpeg installed for on-demand transcoding) the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav` some highlights: * OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png)) * shows the audio waveform in the seekbar * not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended * videos can be played as audio, without wasting bandwidth on the video * adding `?v` to the end of an audio/video/image link will make it open in the mediaplayer click the `play` link next to an audio file, or copy the link target to [share it](https://a.ocv.me/pub/demo/music/Ubiktune%20-%20SOUNDSHOCK%202%20-%20FM%20FUNK%20TERRROR!!/#af-1fbfba61&t=18) (optionally with a timestamp to start playing from, like that example does) open the `[🎺]` media-player-settings tab to configure it, * "switches": * `[🔁]` repeats one single song forever * `[🔀]` shuffles the files inside each folder * `[preload]` starts loading the next track when it's about to end, reduces the silence between songs * `[full]` does a full preload by downloading the entire next file; good for unreliable connections, bad for slow connections * `[~s]` toggles the seekbar waveform display * `[/np]` enables buttons to copy the now-playing info as an irc message * `[📻]` enables buttons to create an [m3u playlist](#playlists) with the selected songs * `[os-ctl]` makes it possible to control audio playback from the lockscreen of your device (enables [mediasession](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession)) * `[seek]` allows seeking with lockscreen controls (buggy on some devices) * `[art]` shows album art on the lockscreen * `[🎯]` keeps the playing song scrolled into view (good when using the player as a taskbar dock) * `[⟎]` shrinks the playback controls * "buttons": * `[uncache]` may fix songs that won't play correctly due to bad files in browser cache * "at end of folder": * `[loop]` keeps looping the folder * `[next]` plays into the next folder * "transcode": * `[flac]` converts `flac` and `wav` files into opus (if supported by browser) or mp3 * `[aac]` converts `aac` and `m4a` files into opus (if supported by browser) or mp3 * `[oth]` converts all other known formats into opus (if supported by browser) or mp3 * `aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|m4b|m4r|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk` * "transcode to": * `[opus]` produces an `opus` whenever transcoding is necessary (the best choice on Android and PCs) * `[awo]` is `opus` in a `weba` file, good for iPhones (iOS 17.5 and newer) but Apple is still fixing some state-confusion bugs as of iOS 18.2.1 * `[caf]` is `opus` in a `caf` file, good for iPhones (iOS 11 through 17), technically unsupported by Apple but works for the most part * `[mp3]` -- the myth, the legend, the undying master of mediocre sound quality that definitely works everywhere * `[flac]` -- lossless but compressed, for LAN and/or fiber playback on electrostatic headphones * `[wav]` -- lossless and uncompressed, for LAN and/or fiber playback on electrostatic headphones connected to very old equipment * `flac` and `wav` must be enabled with `--allow-flac` / `--allow-wav` to allow spending the disk space * "tint" reduces the contrast of the playback bar ### playlists create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists -- see example [text](https://a.ocv.me/pub/demo/music/?doc=example-playlist.m3u) and [player](https://a.ocv.me/pub/demo/music/#m3u=example-playlist.m3u) click a file with the extension `m3u` or `m3u8` (for example `mixtape.m3u` or `touhou.m3u8` ) and you get two choices: Play / Edit playlists can include songs across folders anywhere on the server, but filekeys/dirkeys are NOT supported, so the listener must have read-access or get-access to the files ### creating a playlist with a standalone mediaplayer or copyparty you can use foobar2000, deadbeef, just about any standalone player should work -- but you might need to edit the filepaths in the playlist so they fit with the server-URLs alternatively, you can create the playlist using copyparty itself: * open the `[🎺]` media-player-settings tab and enable the `[📻]` create-playlist feature -- this adds two new buttons in the bottom-right tray, `[📻add]` and `[📻copy]` which appear when you listen to music, or when you select a few audiofiles * click the `📻add` button while a song is playing (or when you've selected some songs) and they'll be added to "the list" (you can't see it yet) * at any time, click `📻copy` to send the playlist to your clipboard * you can then continue adding more songs if you'd like * if you want to wipe the playlist and start from scratch, just refresh the page * create a new textfile, name it `something.m3u` and paste the playlist there ### audio equalizer and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression) can also boost the volume in general, or increase/decrease stereo width (like [crossfeed](https://www.foobar2000.org/components/view/foo_dsp_meiercf) just worse) has the convenient side-effect of reducing the pause between songs, so gapless albums play better with the eq enabled (just make it flat) not available on iPhones / iPads because AudioContext currently breaks background audio playback on iOS (15.7.8) ### fix unreliable playback on android due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one) ## textfile viewer with realtime streaming of logfiles and such ([demo](https://a.ocv.me/pub/demo/logtail/)) , and terminal colors work too click `-txt-` next to a textfile to open the viewer, which has the following toolbar buttons: * `✏️ edit` opens the textfile editor * `📡 follow` starts monitoring the file for changes, streaming new lines in realtime * similar to `tail -f` * [link directly](https://a.ocv.me/pub/demo/logtail/?doc=lipsum.txt&tail) to a file with tailing enabled by adding `&tail` to the textviewer URL ## markdown viewer and there are *two* editors ![copyparty-md-read-fs8](https://user-images.githubusercontent.com/241032/115978057-66419080-a57d-11eb-8539-d2be843991aa.png) there is a built-in extension for inline clickable thumbnails; * enable it by adding `` somewhere in the doc * add thumbnails with `!th[l](your.jpg)` where `l` means left-align (`r` = right-align) * a single line with `---` clears the float / inlining * in the case of README.md being displayed below a file listing, thumbnails will open in the gallery viewer other notes, * the document preview has a max-width which is the same as an A4 paper when printed ### markdown vars dynamic docs with serverside variable expansion to replace stuff like `{{self.ip}}` with the client's IP, or `{{srv.htime}}` with the current time on the server see [./srv/expand/](./srv/expand/) for usage and examples ## other tricks * you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab` * enabling the audio equalizer can help make gapless albums fully gapless in some browsers (chrome), so consider leaving it on with all the values at zero * get a plaintext file listing by adding `?ls=t` to a URL, or a compact colored one with `?ls=v` (for unix terminals) * if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider [./contrib/media-osd-bgone.ps1](contrib/#media-osd-bgoneps1) * click the bottom-left `π` to open a javascript prompt for debugging * files named `.prologue.html` / `.epilogue.html` will be rendered before/after directory listings unless `--no-logues` * files named `descript.ion` / `DESCRIPT.ION` are parsed and displayed in the file listing, or as the epilogue if nonstandard * files named `README.md` / `readme.md` will be rendered after directory listings unless `--no-readme` (but `.epilogue.html` takes precedence) * and `PREADME.md` / `preadme.md` is shown above directory listings unless `--no-readme` or `.prologue.html` * `README.md` and `*logue.html` can contain placeholder values which are replaced server-side before embedding into directory listings; see [`--help-exp`](https://copyparty.eu/cli/#exp-help-page) ## searching search by size, date, path/name, mp3-tags, ... ![copyparty-search-fs8](https://user-images.githubusercontent.com/241032/129635365-c0ff2a9f-0ee5-4fc3-8bb6-006033cf67b8.png) when started with `-e2dsa` copyparty will scan/index all your files. This avoids duplicates on upload, and also makes the volumes searchable through the web-ui: * make search queries by `size`/`date`/`directory-path`/`filename`, or... * drag/drop a local file to see if the same contents exist somewhere on the server, see [file-search](#file-search) path/name queries are space-separated, AND'ed together, and words are negated with a `-` prefix, so for example: * path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path * name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9) the `raw` field allows for more complex stuff such as `( tags like *nhato* or tags like *taishi* ) and ( not tags like *nhato* or not tags like *taishi* )` which finds all songs by either nhato or taishi, excluding collabs (terrible example, why would you do that) for the above example to work, add the commandline argument `-e2ts` to also scan/index tags from music files, which brings us over to: # server config using arguments or config files, or a mix of both: * config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf) * `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting * or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume * changes to the `[global]` config section requires a restart to take effect **NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with [`--help`](https://copyparty.eu/cli/) (or click that link) to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in [`--help-flags`](https://copyparty.eu/cli/#flags-help-page) can be used in volumes as volflags (per-volume configuration). * if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help` * or if you prefer plaintext, https://copyparty.eu/helptext.txt ## version-checker sleep better at night by telling copyparty to periodically check whether your version has a [known vulnerability](https://github.com/9001/copyparty/security/advisories) this feature can be enabled by setting the global-option `--vc-url` to one of the following URLs; all of them provide the same information, so which one you choose is whatever * `https://api.copyparty.eu/advisories` * `https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9` > to see what happens when a bad version is detected, try `--vc-url https://api.copyparty.eu/advisories-test` also consider the following options: * global-option `--vc-age` is how often (in hours) to check that URL; default is 3 * global-option `--vc-exit` can be enabled to panic and immediately exit if a vulnerability is indicated * if `--vc-exit` is not enabled, it just shows a warning on the controlpanel for all users with permission `a` or `A` config file example: ```yaml [global] vc-url: https://api.copyparty.eu/advisories vc-age: 3 # how many hours to wait between each check vc-exit # emergency-exit if current version is vulnerable ``` ## logging serverlog is sent to stdout by default (but logging to a file is also possible) "stdout" usually means either the terminal, or journalctl, or whatever is collecting logs from your docker containers, so that depends on your setup * [-q](https://copyparty.eu/cli/#g-q) disables logging to stdout, and may improve performance a little bit * combine it with `-lo logfolder/cpp-%Y-%m-%d.txt` to log to a file instead * the `%Y-%m-%d` makes it create a new logfile every day, with the date as filename * `-lo whatever.txt` can be used without `-q` to log to both at the same time * by default, the logfile will have colors if the terminal does (usually the case) * use the [textfile-viewer](https://github.com/user-attachments/assets/8a828947-2fae-4df9-bd2a-3de46f42d478) or `less -R` in a terminal to see colors correctly * if you want [no colors](https://youtu.be/biW5UVGkPMA?t=148): * `--flo 2` disables colors for just the logfile * `--no-ansi` disables colors for both the terminal and logfile config file example: ```yaml [global] log-date: %Y-%m-%d # show dates on stdout too lo: /var/log/cpp/%Y-%m-%d.txt # logfile path flo: 2 # just text (no colors) in logfile q # disable stdout; use logfile only ``` ## zeroconf announce enabled services on the LAN ([pic](https://user-images.githubusercontent.com/241032/215344737-0eae8d98-9496-4256-9aa8-cd2f6971810d.png)) -- `-z` enables both [mdns](#mdns) and [ssdp](#ssdp) * `--z-on` / `--z-off` limits the feature to certain networks config file example: ```yaml [global] z # enable all zeroconf features (mdns, ssdp) zm # only enables mdns (does nothing since we already have z) z-on: 192.168.0.0/16, 10.1.2.0/24 # restrict to certain subnets ``` ### mdns LAN domain-name and feature announcer uses [multicast dns](https://en.wikipedia.org/wiki/Multicast_DNS) to give copyparty a domain which any machine on the LAN can use to access it all enabled services ([webdav](#webdav-server), [ftp](#ftp-server), [smb](#smb-server)) will appear in mDNS-aware file managers (KDE, gnome, macOS, ...) the domain will be `partybox.local` if the machine's hostname is `partybox` unless `--name` specifies something else and the web-UI will be available at http://partybox.local:3923/ * if you want to get rid of the `:3923` so you can use http://partybox.local/ instead then see [listen on port 80 and 443](#listen-on-port-80-and-443) ### ssdp windows-explorer announcer uses [ssdp](https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol) to make copyparty appear in the windows file explorer on all machines on the LAN doubleclicking the icon opens the "connect" page which explains how to mount copyparty as a local filesystem if copyparty does not appear in windows explorer, use `--zsv` to see why: * maybe the discovery multicast was sent from an IP which does not intersect with the server subnets ## qr-code print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/194728533-6f00849b-c6ac-43c6-9359-83e454d11e00.png) for quick access, great between phones on android hotspots which keep changing the subnet * `--qr` enables it * `--qrs` does https instead of http * `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2` * `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size * 1x may render incorrectly on some terminals/fonts, but 2x should always work * `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away) * `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt` * `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt` * `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg` * `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png` * `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png` it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain ## ftp server an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit TLS (ftpes) * based on [pyftpdlib](https://github.com/giampaolo/pyftpdlib) * needs a dedicated port (cannot share with the HTTP/HTTPS API) * uploads are not resumable -- delete and restart if necessary * runs in active mode by default, you probably want `--ftp-pr 12000-13000` * if you enable both `ftp` and `ftps`, the port-range will be divided in half * some older software (filezilla on debian-stable) cannot passive-mode with TLS * login with any username + your password, or put your password in the username field * unless you enabled `--usernames` some recommended FTP / FTPS clients; `wark` = example password: * https://winscp.net/eng/download.php * https://filezilla-project.org/ struggles a bit with ftps in active-mode, but is fine otherwise * https://rclone.org/ does FTPS with `tls=false explicit_tls=true` * `lftp -u k,wark -p 3921 127.0.0.1 -e ls` * `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'` * `curl ftp://127.0.0.1:3921/` (plaintext ftp) * `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps) config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall: ```yaml [global] ftp: 3921 ftp-pr: 12000-12099 ``` ## sftp server goes roughly 700 MiB/s (slower than webdav and ftp) > this is **not** [ftps](#ftp-server) (which copyparty also supports); [ftps](#ftp-server) is ftp-tls (think http/https), while **sftp** is ssh-based and (preferably) uses ssh-keys for authentication the sftp-server requires the optional dependency [paramiko](https://pypi.org/project/paramiko/); * if you are **not** using docker, then install paramiko somehow * if you **are** using docker, then use one of the following image variants: `ac` / `im` / `iv` / `dj` enable sftpd with `--sftp 3922` to listen on port 3922; * use global-option `sftp-key` to associate an ssh-key with a user; * commandline: `--sftp-key 'david ssh-ed25519 AAAAC3NzaC...'` * config-file: `sftp-key: david ssh-ed25519 AAAAC3NzaC...` * `--sftp-pw` enables login with passwords (default is ssh-keys only) * `--sftp-anon foo` enables login with username `foo` and no password; gives the same access/permissions as the website does when not logged in see the [sftp section in --help](https://copyparty.eu/cli/#g-sftp) for the other options ## webdav server with read-write support, supports winXP and later, macos, nautilus/gvfs ... a great way to [access copyparty straight from the file explorer in your OS](#mount-as-drive) click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos general usage: * login with any username + your password, or put your password in the username field (password field can be empty/whatever) * unless you enabled `--usernames` on macos, connect from finder: * [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/ to be able to edit existing files, the client must have the Delete-permission, and some webdav clients will also require the [daw](https://copyparty.eu/cli/#g-daw) volflag or global-option (not necessary if the client sends the `x-oc-mtime` header). Without `daw`, those clients will fail to modify existing files and instead create new copies with names like `notes.txt-1771978661.726032-3i9GPghL.txt`. **NOTE:** Enabling `daw` will also make all PUT-uploads overwrite existing files if the user has delete-access, so use with caution. Another alternative is the [dav-port](https://copyparty.eu/cli/#g-dav-port) option > note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients) ### connecting to webdav from windows using the GUI (winXP or later): * rightclick [my computer] -> [map network drive] -> Folder: `http://192.168.123.1:3923/` * on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there * providing your password as the username is recommended; the password field can be anything or empty * unless you enabled `--usernames` the webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead: * win7+ doesn't actually send the password to the server when reauthenticating after a reboot unless you first try to login with an incorrect password and then switch to the correct password * or just type your password into the username field instead to get around it entirely * connecting to a folder which allows anonymous read will make writing impossible, as windows has decided it doesn't need to login * workaround: connect twice; first to a folder which requires auth, then to the folder you actually want, and leave both of those mounted * or set the server-option `--dav-auth` to force password-auth for all webdav clients * win7+ may open a new tcp connection for every file and sometimes forgets to close them, eventually needing a reboot * maybe NIC-related (??), happens with win10-ltsc on e1000e but not virtio * windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:"/\|?*`), or names ending with `.` * winxp cannot show unicode characters outside of *some range* * latin-1 is fine, hiragana is not (not even as shift-jis on japanese xp) ## tftp server a TFTP server (read/write) can be started using `--tftp 3969` (you probably want [ftp](#ftp-server) instead unless you are *actually* communicating with hardware from the 90s (in which case we should definitely hang some time)) > that makes this the first RTX DECT Base that has been updated using copyparty 🎉 * based on [partftpy](https://github.com/9001/partftpy) * no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable * needs a dedicated port (cannot share with the HTTP/HTTPS API) * run as root (or see below) to use the spec-recommended port `69` (nice) * can reply from a predefined portrange (good for firewalls) * only supports the binary/octet/image transfer mode (no netascii) * [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN * assuming default blksize (512), expect 1100 KiB/s over 100BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi most clients expect to find TFTP on port 69, but on linux and macos you need to be root to listen on that. Alternatively, listen on 3969 and use NAT on the server to forward 69 to that port; * on linux: `iptables -t nat -A PREROUTING -i eth0 -p udp --dport 69 -j REDIRECT --to-port 3969` some recommended TFTP clients: * curl (cross-platform, read/write) * get: `curl --tftp-blksize 1428 tftp://127.0.0.1:3969/firmware.bin` * put: `curl --tftp-blksize 1428 -T firmware.bin tftp://127.0.0.1:3969/` * windows: `tftp.exe` (you probably already have it) * `tftp -i 127.0.0.1 put firmware.bin` * linux: `tftp-hpa`, `atftp` * `atftp --option "blksize 1428" 127.0.0.1 3969 -p -l firmware.bin -r firmware.bin` * `tftp -v -m binary 127.0.0.1 3969 -c put firmware.bin` ## smb server unsafe, slow, not recommended for wan, enable with `--smb` for read-only or `--smbw` for read-write click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to see connection instructions for windows, linux, macos dependencies: `python3 -m pip install --user -U impacket==0.13.0` * newer versions of impacket will hopefully work just fine but there is monkeypatching so maybe not some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance: * not entirely confident that read-only is read-only * the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh) or [bubbleparty](./bin/bubbleparty.sh) * account passwords work per-volume as expected, and so does account permissions (read/write/move/delete), but `--smbw` must be given to allow write-access from smb * [shadowing](#shadowing) probably works as expected but no guarantees * not compatible with pw-hashing or `--usernames` and some minor issues, * clients only see the first ~400 files in big folders; * this was originally due to [impacket#1433](https://github.com/SecureAuthCorp/impacket/issues/1433) which was fixed in impacket-0.12, so you can disable the workaround with `--smb-nwa-1` but then you get unacceptably poor performance instead * hot-reload of server config (`/?reload=cfg`) does not include the `[global]` section (commandline args) * listens on the first IPv4 `-i` interface only (default = :: = 0.0.0.0 = all) * login doesn't work on winxp, but anonymous access is ok -- remove all accounts from copyparty config for that to work * win10 onwards does not allow connecting anonymously / without accounts * python3 only * slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster) * those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav known client bugs: * on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2 * however smb1 is buggy and is not enabled by default on win10 onwards * windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:"/\|?*`), or names ending with `.` the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT on the server to forward the traffic from 445 to there; * on linux: `iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 445 -j REDIRECT --to-port 3945` authenticate with one of the following: * username `$username`, password `$password` * username `$password`, password `k` ## browser ux tweaking the ui * set default sort order globally with `--sort` or per-volume with the `sort` volflag; specify one or more comma-separated columns to sort by, and prefix the column name with `-` for reverse sort * the column names you can use are visible as tooltips when hovering over the column headers in the directory listing, for example `href ext sz ts tags/.up_at tags/Circle tags/.tn tags/Artist tags/Title` * to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Circle,tags/.tn,tags/Artist,tags/Title,href` * to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at` see [./docs/rice](./docs/rice) for more, including: * how to [hide ui-elements](./docs/rice/README.md#hide-ui-elements) * [custom fonts](./docs/rice/README.md#custom-fonts) * [custom loading-spinner](./docs/rice/README.md#boring-loader-spinner) * adding stuff (css/``/...) [to the html `` tag](./docs/rice/README.md#head) * [adding your own translation](./docs/rice/README.md#translations) ## opengraph discord and social-media embeds can be enabled globally with `--og` or per-volume with volflag `og` note that this disables hotlinking because the opengraph spec demands it; to sneak past this intentional limitation, you can enable opengraph selectively by user-agent, for example `--og-ua '(Discord|Twitter|Slack)bot'` (or volflag `og_ua`) you can also hotlink files regardless by appending `?raw` to the url > WARNING: if you plan to use WebDAV, then `--og-ua` / `og_ua` must be configured if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object) ## file deduplication enable symlink-based upload deduplication globally with `--dedup` or per-volume with volflag `dedup` by default, when someone tries to upload a file that already exists on the server, the upload will be politely declined, and the server will copy the existing file over to where the upload would have gone if you enable deduplication with `--dedup` then it'll create a symlink instead of a full copy, thus reducing disk space usage * on the contrary, if your server is hooked up to s3-glacier or similar storage where reading is expensive, and you cannot use `--safe-dedup=1` because you have other software tampering with your files, so you want to entirely disable detection of duplicate data instead, then you can specify `--no-clone` globally or `noclone` as a volflag **warning:** when enabling dedup, you should also: * enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended * ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below * ...and/or `--reflink` to use CoW/reflink-based dedup (much safer than hardlink, but OS/FS-dependent) it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to) by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`, and you can choose to use reflinks with `--reflink` or volflag `reflink` advantages of using reflinks (CoW, copy-on-write): * entirely safe (when your filesystem supports it correctly); either file can be edited or deleted without affecting other copies * only linux 5.3 or newer, only python 3.14 or newer, only some filesystems (btrfs probably ok, maybe xfs too, but zfs had bugs) advantages of using hardlinks: * hardlinks are more compatible with other software; they behave entirely like regular files * you can safely move and rename files using other file managers * symlinks need to be managed by copyparty to ensure the destinations remain correct advantages of using symlinks (default): * each symlink can have its own last-modified timestamp, but a single timestamp is shared by all hardlinks * symlinks make it more obvious to other software that the file is not a regular file, so this can be less dangerous * hardlinks look like regular files, so other software may assume they are safe to edit without affecting the other copies **warning:** if you edit the contents of a deduplicated file, then you will also edit all other copies of that file! This is especially surprising with hardlinks, because they look like regular files, but that same file exists in multiple locations global-option `--xlink` / volflag `xlink` additionally enables deduplication across volumes, but this is probably buggy and not recommended config file example: ```yaml [global] e2dsa # scan and index filesystem on startup dedup # symlink-based deduplication for all volumes [/media] /mnt/nas/media flags: hardlinkonly # this vol does hardlinks instead of symlinks ``` ## file indexing enable music search, upload-undo, and better dedup file indexing relies on two database tables, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`), stored in `.hist/up2k.db`. Configuration can be done through arguments, volflags, or a mix of both. through arguments: * `-e2d` enables file indexing on upload * `-e2ds` also scans writable folders for new files on startup * `-e2dsa` also scans all mounted volumes (including readonly ones) * `-e2t` enables metadata indexing on upload * `-e2ts` also scans for tags in all files that don't have tags yet * `-e2tsr` also deletes all existing tags, doing a full reindex * `-e2v` verifies file integrity at startup, comparing hashes from the db * `-e2vu` patches the database with the new hashes from the filesystem * `-e2vp` panics and kills copyparty instead the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling: * `-v ~/music::r:c,e2ds,e2tsr` does a full reindex of everything on startup * `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on * `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*` * `-v ~/music::r:c,d2ds` disables on-boot scans; only index new uploads * `-v ~/music::r:c,d2ts` same except only affecting tags note: * upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings) * and file checksums can be shown with global-option `-e2d -mte +w` or volflag `e2d,mte=+w` (always active for users with permission `a`) * `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise config file example (these options are recommended btw): ```yaml [global] e2dsa # scan and index all files in all volumes on startup e2ts # check newly-discovered or uploaded files for media tags ``` ### exclude-patterns to save some time, you can provide a regex pattern for filepaths to only index by filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash '\.iso$'` or the volflag `:c,nohash=\.iso$`, this has the following consequences: * initial indexing is way faster, especially when the volume is on a network disk * makes it impossible to [file-search](#file-search) * if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected similarly, you can fully ignore files/folders using `--no-idx [...]` and `:c,noidx=\.iso$` NOTE: `no-idx` and/or `no-hash` prevents deduplication of those files * when running on macos, all the usual apple metadata files are excluded by default if you set `--no-hash [...]` globally, you can enable hashing for specific volumes using flag `:c,nohash=` to exclude certain filepaths from search-results, use `--srch-excl` or volflag `srch_excl` instead of `--no-idx`, for example `--srch-excl 'password|logs/[0-9]'` config file example: ```yaml [/games] /mnt/nas/games flags: noidx: \.iso$ # skip indexing iso-files srch_excl: password|logs/[0-9] # filter search results ``` ### filesystem guards avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, skipping any symlinks or bind-mounts to another HDD for example and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere * symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer ### periodic rescan filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date argument `--re-maxage 60` will rescan all volumes every 60 sec, same as volflag `:c,scan=60` to specify it per-volume uploads are disabled while a rescan is happening, so rescans will be delayed by `--db-act` (default 10 sec) when there is write-activity going on (uploads, renames, ...) note: folder-thumbnails are selected during filesystem indexing, so periodic rescans can be used to keep them accurate as images are uploaded/deleted (or manually do a rescan with the `reload` button in the controlpanel) config file example: ```yaml [global] re-maxage: 3600 [/pics] /mnt/nas/pics flags: scan: 900 ``` ## upload rules set upload rules using volflags, some examples: * `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`) * `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards * `:c,vmaxb=1g` block uploads if total volume size would exceed 1 GiB afterwards * `:c,vmaxn=4k` block uploads if volume would contain more than 4096 files afterwards * `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`: * `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1) * `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format * `:c,rotf_tz=Europe/Oslo` sets the timezone (default is UTC unless global-option `rotf-tz` is changed) * if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example * but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string * just to avoid additional complexity in up2k which is enough of a mess already * `:c,lifetime=300` delete uploaded files when they become 5 minutes old you can also set transaction limits which apply per-IP and per-volume, but these assume `-j 1` (default) otherwise the limits will be messed up, for example `-j 4` would allow anywhere between 1x and 4x the limits you set depending on which processing node the client gets routed to * `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume) * `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume) notes: * `vmaxb` and `vmaxn` requires either the `e2ds` volflag or `-e2dsa` global-option config file example: ```yaml [/inc] /mnt/nas/uploads accs: w: * # anyone can upload here rw: ed # only user "ed" can read-write flags: e2ds # filesystem indexing is required for many of these: sz: 1k-3m # accept upload only if filesize in this range df: 4g # free disk space cannot go lower than this vmaxb: 1g # volume can never exceed 1 GiB vmaxn: 4k # ...or 4000 files, whichever comes first nosub # must upload to toplevel folder lifetime: 300 # uploads are deleted after 5min maxn: 250,3600 # each IP can upload 250 files in 1 hour maxb: 1g,300 # each IP can upload 1 GiB over 5 minutes ``` ## compress uploads files can be autocompressed on upload, either on user-request (if config allows) or forced by server-config * volflag `gz` allows gz compression * volflag `xz` allows lzma compression * volflag `pk` **forces** compression on all files * url parameter `pk` requests compression with server-default algorithm * url parameter `gz` or `xz` requests compression with a specific algorithm * url parameter `xz` requests xz compression things to note, * the `gz` and `xz` arguments take a single optional argument, the compression level (range 0 to 9) * the `pk` volflag takes the optional argument `ALGORITHM,LEVEL` which will then be forced for all uploads, for example `gz,9` or `xz,0` * default compression is gzip level 9 * all upload methods except up2k are supported * the files will be indexed after compression, so dupe-detection and file-search will not work as expected some examples, * `-v inc:inc:w:c,pk=xz,0` folder named inc, shared at inc, write-only for everyone, forces xz compression at level 0 * `-v inc:inc:w:c,pk` same write-only inc, but forces gz compression (default) instead of xz * `-v inc:inc:w:c,gz` 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` * needs https://pypi.org/project/python-magic/ `python3 -m pip install --user -U python-magic` * on windows grab this instead `python3 -m pip install --user -U python-magic-bin` * `cachectl` changes how webbrowser will cache responses (the `Cache-Control` response-header); default is `no-cache` which will prevent repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed) * adding `?cache` to a link will override this with "fully cache this for 69 seconds"; `?cache=321` is 321 seconds, and `?cache=i` is 7 days ## database location in-volume (`.hist/up2k.db`, default) or somewhere else copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff this can instead be kept in a single place using the `--hist` argument, or the `hist=` volflag, or a mix of both: * `--hist ~/.cache/copyparty -v ~/music::r:c,hist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior) by default, the per-volume `up2k.db` sqlite3-database for `-e2d` and `-e2t` is stored next to the thumbnails according to the `--hist` option, but the global-option `--dbpath` and/or volflag `dbpath` can be used to put the database somewhere else if your storage backend is unreliable (NFS or bad HDDs), you can specify one or more "landmarks" to look for before doing anything database-related. A landmark is a file which is always expected to exist inside the volume. This avoids spurious filesystem rescans in the event of an outage. One line per landmark (see example below) note: * putting the hist-folders on an SSD is strongly recommended for performance * markdown edits are always stored in a local `.hist` subdirectory * on windows the volflag path is cyglike, so `/c/temp` means `C:\temp` but use regular paths for `--hist` * you can use cygpaths for volumes too, `-v C:\Users::r` and `-v /c/users::r` both work config file example: ```yaml [global] hist: ~/.cache/copyparty # put db/thumbs/etc. here by default [/pics] /mnt/nas/pics flags: hist: - # restore the default (/mnt/nas/pics/.hist/) hist: /mnt/nas/cache/pics/ # can be absolute path landmark: me.jpg # /mnt/nas/pics/me.jpg must be readable to enable db landmark: info/a.txt^=ok # and this textfile must start with "ok" ``` ## metadata from audio files set `-e2t` to index tags on upload `-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume: * `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist* if you add/remove a tag from `mte` you will need to run with `-e2tsr` once to rebuild the database, otherwise only new files will be affected but instead of using `-mte`, `-mth` is a better way to hide tags in the browser: these tags will not be displayed by default, but they still get indexed and become searchable, and users can choose to unhide them in the `[⚙️] config` pane `-mtm` can be used to add or redefine a metadata mapping, say you have media files with `foo` and `bar` tags and you want them to display as `qux` in the browser (preferring `foo` if both are present), then do `-mtm qux=foo,bar` and now you can `-mte artist,title,qux` tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,) `--no-mutagen` disables Mutagen and uses FFprobe instead, which... * is about 20x slower than Mutagen * catches a few tags that Mutagen doesn't * melodic key, video resolution, framerate, pixfmt * avoids pulling any GPL code into copyparty * more importantly runs FFprobe on incoming files which is bad if your FFmpeg has a cve `--mtag-to` sets the tag-scan timeout; very high default (60 sec) to cater for zfs and other randomly-freezing filesystems. Lower values like 10 are usually safe, allowing for faster processing of tricky files ### metadata from xattrs unix extended file attributes (Linux-only) can be indexed into the db and made searchable; * `--db-xattr user.foo,user.bar` will index the xattrs `user.foo` and `user.bar`, * `--db-xattr user.foo=foo,user.bar=bar` will index them with the names `foo` and `bar`, * `--db-xattr ~~user.foo,user.bar` will index everything *except* `user.foo` and `user.bar`, * `--db-xattr ~~` will index everything however note that the tags must also be enabled with `-mte` so here are some complete examples: * `-e2ts --db-xattr user.foo,user.bar -mte +user.foo,user.bar` * `-e2ts --db-xattr user.foo=foo,user.bar=bar -mte +foo,bar` as for actually adding the xattr `user.foo` to a file in the first place, * `setfattr -n user.foo -v 'utsikt fra fløytoppen' photo.jpg` ## file parser plugins provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md) copyparty can invoke external programs to collect additional metadata for files using `mtp` (either as argument or volflag), there is a default timeout of 60sec, and only files which contain audio get analyzed by default (see ay/an/ad below) * `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata * `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`) * `-v ~/music::r:c,mtp=.bpm=~/bin/audio-bpm.py:c,mtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly *but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files (default), `an` only do non-audio files, or `ad` do all files (d as in dontcare) * "audio file" also means videos btw, as long as there is an audio stream * `-mtp ext=an,~/bin/file-ext.py` runs `~/bin/file-ext.py` to get the `ext` tag only if file is not audio (`an`) * `-mtp arch,built,ver,orig=an,eexe,edll,~/bin/exe.py` runs `~/bin/exe.py` to get properties about windows-binaries only if file is not audio (`an`) and file extension is exe or dll * if you want to daisychain parsers, use the `p` flag to set processing order * `-mtp foo=p1,~/a.py` runs before `-mtp foo=p2,~/b.py` and will forward all the tags detected so far as json to the stdin of b.py * option `c0` disables capturing of stdout/stderr, so copyparty will not receive any tags from the process at all -- instead the invoked program is free to print whatever to the console, just using copyparty as a launcher * `c1` captures stdout only, `c2` only stderr, and `c3` (default) captures both * you can control how the parser is killed if it times out with option `kt` killing the entire process tree (default), `km` just the main process, or `kn` let it continue running until copyparty is terminated if something doesn't work, try `--mtag-v` for verbose error messages config file example; note that `mtp` is an additive option so all of the mtp options will take effect: ```yaml [/music] /mnt/nas/music flags: mtp: .bpm=~/bin/audio-bpm.py # assign ".bpm" (numeric) with script mtp: key=f,t5,~/bin/audio-key.py # force/overwrite, 5sec timeout mtp: ext=an,~/bin/file-ext.py # will only run on non-audio files mtp: arch,built,ver,orig=an,eexe,edll,~/bin/exe.py # only exe/dll ``` ## event hooks trigger a program on uploads, renames etc ([examples](./bin/hooks/)) you can set hooks before and/or after an event happens, and currently you can hook uploads, moves/renames, and deletes there's a bunch of flags and stuff, see [`--help-hooks`](https://copyparty.eu/cli/#hooks-help-page) if you want to write your own hooks, see [devnotes](./docs/devnotes.md#event-hooks) ### zeromq event-hooks can send zeromq messages instead of running programs to send a 0mq message every time a file is uploaded, * `--xau zmq:pub:tcp://*:5556` sends a PUB to any/all connected SUB clients * `--xau t3,zmq:push:tcp://*:5557` sends a PUSH to exactly one connected PULL client * `--xau t3,j,zmq:req:tcp://localhost:5555` sends a REQ to the connected REP client the PUSH and REQ examples have `t3` (timeout after 3 seconds) because they block if there's no clients to talk to * the REQ example does `t3,j` to send extended upload-info as json instead of just the filesystem-path see [zmq-recv.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/zmq-recv.py) if you need something to receive the messages with config file example; note that the hooks are additive options, so all of the xau options will take effect: ```yaml [global] xau: zmq:pub:tcp://*:5556` # send a PUB to any/all connected SUB clients xau: t3,zmq:push:tcp://*:5557` # send PUSH to exactly one connected PULL cli xau: t3,j,zmq:req:tcp://localhost:5555` # send REQ to the connected REP cli ``` ### upload events the older, more powerful approach ([examples](./bin/mtag/)): ``` -v /mnt/inc:inc:w:c,e2d,e2t,mte=+x1:c,mtp=x1=ad,kn,/usr/bin/notify-send ``` that was the commandline example; here's the config file example: ```yaml [/inc] /mnt/inc accs: w: * flags: e2d, e2t # enable indexing of uploaded files and their tags mte: +x1 mtp: x1=ad,kn,/usr/bin/notify-send ``` so filesystem location `/mnt/inc` shared at `/inc`, write-only for everyone, appending `x1` to the list of tags to index (`mte`), and using `/usr/bin/notify-send` to "provide" tag `x1` for any filetype (`ad`) with kill-on-timeout disabled (`kn`) that'll run the command `notify-send` with the path to the uploaded file as the first and only argument (so on linux it'll show a notification on-screen) note that this is way more complicated than the new [event hooks](#event-hooks) but this approach has the following advantages: * non-blocking and multithreaded; doesn't hold other uploads back * you get access to tags from FFmpeg and other mtp parsers * only trigger on new unique files, not dupes note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1` for reference, if you were to do this using event hooks instead, it would be like this: `-e2d --xau notify-send,hello,--` ## handlers redefine behavior with plugins ([examples](./bin/handlers/)) replace 404 and 403 errors with something completely different (that's it for now) as for client-side stuff, there is [plugins for modifying UI/UX](./contrib/plugins/) ## ip auth autologin based on IP range (CIDR) , using the global-option `--ipu` for example, if everyone with an IP that starts with `192.168.123` should automatically log in as the user `spartacus`, then you can either specify `--ipu=192.168.123.0/24=spartacus` as a commandline option, or put this in a config file: ```yaml [global] ipu: 192.168.123.0/24=spartacus ``` repeat the option to map additional subnets **be careful with this one!** if you have a reverseproxy, then you definitely want to make sure you have [real-ip](#real-ip) configured correctly, and it's probably a good idea to nullmap the reverseproxy's IP just in case; so if your reverseproxy is sending requests from `172.24.27.9` then that would be `--ipu=172.24.27.9/32=` ### restrict to ip limit a user to certain IP ranges (CIDR) , using the global-option `--ipr` for example, if the user `spartacus` should get rejected if they're not connecting from an IP that starts with `192.168.123` or `172.16`, then you can either specify `--ipr=192.168.123.0/24,172.16.0.0/16=spartacus` as a commandline option, or put this in a config file: ```yaml [global] ipr: 192.168.123.0/24,172.16.0.0/16=spartacus ``` repeat the option to map additional users ## identity providers replace copyparty passwords with oauth and such you can disable the built-in password-based login system, and instead replace it with a separate piece of software (an identity provider) which will then handle authenticating / authorizing of users; this makes it possible to login with passkeys / fido2 / webauthn / yubikey / ldap / active directory / oauth / many other single-sign-on contraptions * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header * `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password * the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login * if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type) a more complete example of the copyparty configuration options [look like this](./docs/examples/docker/idp/copyparty.conf) but if you just want to let users change their own passwords, then you probably want [user-changeable passwords](#user-changeable-passwords) instead ### generic header auth other ways to auth by header if you have a middleware which adds a header with a user identifier, for example tailscale's `Tailscale-User-Login: alice.m@forest.net` then you can automatically auth as `alice` by defining that mapping with `--idp-hm-usr '^Tailscale-User-Login^alice.m@forest.net^alice'` or the following config file: ```yaml [global] idp-hm-usr: ^Tailscale-User-Login^alice.m@forest.net^alice ``` repeat the whole `idp-hm-usr` option to add more mappings ## user-changeable passwords if permitted, users can change their own passwords in the control-panel * not compatible with [identity providers](#identity-providers) * must be enabled with `--chpw` because account-sharing is a popular usecase * if you want to enable the feature but deny password-changing for a specific list of accounts, you can do that with `--chpw-no name1,name2,name3,...` * to perform a password reset, edit the server config and give the user another password there, then do a [config reload](#server-config) or server restart * the custom passwords are kept in a textfile at filesystem-path `--chpw-db`, by default `chpw.json` in the copyparty config folder * if you run multiple copyparty instances with different users you *almost definitely* want to specify separate DBs for each instance * if [password hashing](#password-hashing) is enabled, the passwords in the db are also hashed * ...which means that all user-defined passwords will be forgotten if you change password-hashing settings ## using the cloud as storage connecting to an aws s3 bucket and similar there is no built-in support for this, but you can use FUSE-software such as [rclone](https://rclone.org/) / [geesefs](https://github.com/yandex-cloud/geesefs) / [JuiceFS](https://juicefs.com/en/) to first mount your cloud storage as a local disk, and then let copyparty use (a folder in) that disk as a volume if copyparty is unable to access the local folder that rclone/geesefs/JuiceFS provides (for example if it looks invisible) then you may need to run rclone with `--allow-other` and/or enable `user_allow_other` in `/etc/fuse.conf` you will probably get decent speeds with the default config, however most likely restricted to using one TCP connection per file, so the upload-client won't be able to send multiple chunks in parallel > before [v1.13.5](https://github.com/9001/copyparty/releases/tag/v1.13.5) it was recommended to use the volflag `sparse` to force-allow multiple chunks in parallel; this would improve the upload-speed from `1.5 MiB/s` to over `80 MiB/s` at the risk of provoking latent bugs in S3 or JuiceFS. But v1.13.5 added chunk-stitching, so this is now probably much less important. On the contrary, `nosparse` *may* now increase performance in some cases. Please try all three options (default, `sparse`, `nosparse`) as the optimal choice depends on your network conditions and software stack (both the FUSE-driver and cloud-server) someone has also tested geesefs in combination with [gocryptfs](https://nuetzlich.net/gocryptfs/) with surprisingly good results, getting 60 MiB/s upload speeds on a gbit line, but JuiceFS won with 80 MiB/s using its built-in encryption you may improve performance by specifying larger values for `--iobuf` / `--s-rd-sz` / `--s-wr-sz` > if you've experimented with this and made interesting observations, please share your findings so we can add a section with specific recommendations :-) ## hiding from google tell search engines you don't wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings: * `--no-robots` adds HTTP (`X-Robots-Tag`) and HTML (``) headers with `noindex, nofollow` globally * volflag `[...]:c,norobots` does the same thing for that single volume * volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally also, `--force-js` disables the plain HTML folder listing, making things harder to parse for *some* search engines -- note that crawlers which understand javascript (such as google) will not be affected ## themes you can change the default theme with `--theme 2`, and add your own themes by modifying `browser.css` or providing your own css to `--css-browser`, then telling copyparty they exist by increasing `--themes`
0. classic dark 2. flat pm-monokai 4. vice
1. classic light 3. flat light 5. hotdog stand
the classname of the HTML tag is set according to the selected theme, which is used to set colors as css variables ++ * each theme *generally* has a dark theme (even numbers) and a light theme (odd numbers), showing in pairs * the first theme (theme 0 and 1) is `html.a`, second theme (2 and 3) is `html.b` * if a light theme is selected, `html.y` is set, otherwise `html.z` is * so if the dark edition of the 2nd theme is selected, you use any of `html.b`, `html.z`, `html.bz` to specify rules see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where the color variables are set, and there's layout-specific stuff near the bottom if you want to change the fonts, see [./docs/rice/](./docs/rice/) ## complete examples * see [running on windows](./docs/examples/windows.md) for a fancy windows setup * or use any of the examples below, just replace `python copyparty-sfx.py` with `copyparty.exe` if you're using the exe edition * allow anyone to download or upload files into the current folder: `python copyparty-sfx.py` * enable searching and music indexing with `-e2dsa -e2ts` * start an FTP server on port 3921 with `--ftp 3921` * announce it on your LAN with `-z` so it appears in windows/Linux file managers * anyone can upload, but nobody can see any files (even the uploader): `python copyparty-sfx.py -e2dsa -v .::w` * block uploads if there's less than 4 GiB free disk space with `--df 4` * show a popup on new uploads with `--xau bin/hooks/notify.py` * anyone can upload, and receive "secret" links for each upload they do: `python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8` * anyone can browse (`r`), only `kevin` (password `okgo`) can upload/move/delete (`A`) files: `python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:A,kevin` * read-only music server: `python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2` * ...with bpm and key scanning `-mtp .bpm=f,audio-bpm.py -mtp key=f,audio-key.py` * ...with a read-write folder for `kevin` whose password is `okgo` `-a kevin:okgo -v /mnt/nas/inc:/inc:rw,kevin` * ...with logging to disk `-lo log/cpp-%Y-%m%d-%H%M%S.txt.xz` ## listen on port 80 and 443 become a *real* webserver which people can access by just going to your IP or domain without specifying a port **if you're on windows,** then you just need to add the commandline argument `-p 80,443` and you're done! nice **if you're on macos,** sorry, I don't know **if you're on Linux,** you have the following 4 options: * **option 1:** set up a [reverse-proxy](#reverse-proxy) -- this one makes a lot of sense if you're running on a proper headless server, because that way you get real HTTPS too * **option 2:** NAT to port 3923 -- this is cumbersome since you'll need to do it every time you reboot, and the exact command may depend on your linux distribution: ```bash iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3923 iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 3923 ``` * **option 3:** disable the [security policy](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html) which prevents the use of 80 and 443; this is *probably* fine: ``` setcap CAP_NET_BIND_SERVICE=+eip $(realpath $(which python)) python copyparty-sfx.py -p 80,443 ``` * **option 4:** run copyparty as root (please don't) ## reverse-proxy running copyparty next to other websites hosted on an existing webserver such as nginx, caddy, or apache you can either: * give copyparty its own domain or subdomain (recommended) * or do location-based proxying, using `--rp-loc=/stuff` to tell copyparty where it is mounted -- has a slight performance cost and higher chance of bugs * if copyparty says `incorrect --rp-loc or webserver config; expected vpath starting with [...]` it's likely because the webserver is stripping away the proxy location from the request URLs -- see the `ProxyPass` in the apache example below when running behind a reverse-proxy (this includes services like cloudflare), it is important to configure real-ip correctly, as many features rely on knowing the client's IP. The best/safest approach is to configure your reverse-proxy so it gives copyparty a header which only contains the client's true/real IP-address, and then setting `--xff-hdr theHeaderName --rproxy 1` but alternatively, if you want/need to let copyparty handle this, look out for red and yellow log messages which explain how to do that. Basically, the log will say this: > set `--xff-hdr` to the name of the http-header to read the IP from (usually `x-forwarded-for`, but cloudflare uses `cf-connecting-ip`), and then `--xff-src` to the IP of the reverse-proxy so copyparty will trust the xff-hdr. You will also need to configure `--rproxy` to `1` if the header only contains one IP (the correct one) or to a *negative value* if it contains multiple; `-1` being the rightmost and most trusted IP (the nearest proxy, so usually not the correct one), `-2` being the second-closest hop, and so on Note that `--rp-loc` in particular will not work at all unless you configure the above correctly some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatically obtain a valid https/tls certificate for you, and some support HTTP/2 and QUIC which *could* be a nice speed boost, depending on a lot of factors * **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now * depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2 for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/dev/shm/party.sock` (permission `770` means only members of group `www` can access it) example webserver / reverse-proxy configs: * [apache config](contrib/apache/copyparty.conf) * caddy uds: `caddy reverse-proxy --from :8080 --to unix///dev/shm/party.sock` * caddy tcp: `caddy reverse-proxy --from :8081 --to http://127.0.0.1:3923` * [haproxy config](contrib/haproxy/copyparty.conf) * [lighttpd subdomain](contrib/lighttpd/subdomain.conf) -- entire domain/subdomain * [lighttpd subpath](contrib/lighttpd/subpath.conf) -- location-based (not optimal, but in case you need it) * [nginx config](contrib/nginx/copyparty.conf) -- recommended * [traefik config](contrib/traefik/copyparty.yaml) -- only use v3.6.7 or newer [due to CVE-2025-66490](https://github.com/9001/copyparty/issues/1205) ### real-ip teaching copyparty how to see client IPs when running behind a reverse-proxy, or a WAF, or another protection service such as cloudflare if you (and maybe everybody else) keep getting a message that says `thank you for playing`, then you've gotten banned for malicious traffic. This ban applies to the IP address that copyparty *thinks* identifies the shady client -- so, depending on your setup, you might have to tell copyparty where to find the correct IP for most common setups, there should be a helpful message in the server-log explaining what to do, but see [docs/xff.md](docs/xff.md) if you want to learn more, including a quick hack to **just make it work** (which is **not** recommended, but hey...) ### reverse-proxy performance most reverse-proxies support connecting to copyparty either using uds/unix-sockets (`/dev/shm/party.sock`, faster/recommended) or using tcp (`127.0.0.1`) with copyparty listening on a uds / unix-socket / unix-domain-socket and the reverse-proxy connecting to that: | index.html | upload | download | software | | ------------ | ----------- | ----------- | -------- | | 28'900 req/s | 6'900 MiB/s | 7'400 MiB/s | no-proxy | | 18'750 req/s | 3'500 MiB/s | 2'370 MiB/s | haproxy | | 9'900 req/s | 3'750 MiB/s | 2'200 MiB/s | caddy | | 18'700 req/s | 2'200 MiB/s | 1'570 MiB/s | nginx | | 9'700 req/s | 1'750 MiB/s | 1'830 MiB/s | apache | | 9'900 req/s | 1'300 MiB/s | 1'470 MiB/s | lighttpd | when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-fasioned way), speeds are a bit worse: | index.html | upload | download | software | | ------------ | ----------- | ----------- | -------- | | 21'200 req/s | 5'700 MiB/s | 6'700 MiB/s | no-proxy | | 14'500 req/s | 1'700 MiB/s | 2'170 MiB/s | haproxy | | 11'100 req/s | 2'750 MiB/s | 2'000 MiB/s | traefik | | 8'400 req/s | 2'300 MiB/s | 1'950 MiB/s | caddy | | 13'400 req/s | 1'100 MiB/s | 1'480 MiB/s | nginx | | 8'400 req/s | 1'000 MiB/s | 1'000 MiB/s | apache | | 6'500 req/s | 1'270 MiB/s | 1'500 MiB/s | lighttpd | in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet) * if these results are bullshit because my config examples are bad, please submit corrections! ## permanent cloudflare tunnel if you have a domain and want to get your copyparty online real quick, either from your home-PC behind a CGNAT or from a server without an existing [reverse-proxy](#reverse-proxy) setup, one approach is to create a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) (formerly "Argo Tunnel") I'd recommend making a `Locally-managed tunnel` for more control, but if you prefer to make a `Remotely-managed tunnel` then this is currently how: * `cloudflare dashboard` » `zero trust` » `networks` » `tunnels` » `create a tunnel` » `cloudflared` » choose a cool `subdomain` and leave the `path` blank, and use `service type` = `http` and `URL` = `127.0.0.1:3923` * and if you want to just run the tunnel without installing it, skip the `cloudflared service install BASE64` step and instead do `cloudflared --no-autoupdate tunnel run --token BASE64` NOTE: since people will be connecting through cloudflare, as mentioned in [real-ip](#real-ip) you should run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly config file example: ```yaml [global] xff-hdr: cf-connecting-ip ``` ## prometheus metrics/stats can be enabled at URL `/.cpr/metrics` for grafana / prometheus / etc (openmetrics 1.0.0) must be enabled with `--stats` since it reduces startup time a tiny bit, and you probably want `-e2dsa` too the endpoint is only accessible by `admin` accounts, meaning the `a` in `rwmda` in the following example commandline: `python3 -m copyparty -a ed:wark -v /mnt/nas::rwmda,ed --stats -e2dsa` follow a guide for setting up `node_exporter` except have it read from copyparty instead; example `/etc/prometheus/prometheus.yml` below ```yaml scrape_configs: - job_name: copyparty metrics_path: /.cpr/metrics basic_auth: password: wark static_configs: - targets: ['192.168.123.1:3923'] ``` currently the following metrics are available, * `cpp_uptime_seconds` time since last copyparty restart * `cpp_boot_unixtime_seconds` same but as an absolute timestamp * `cpp_active_dl` number of active downloads * `cpp_http_conns` number of open http(s) connections * `cpp_http_reqs` number of http(s) requests handled * `cpp_sus_reqs` number of 403/422/malicious requests * `cpp_active_bans` number of currently banned IPs * `cpp_total_bans` number of IPs banned since last restart these are available unless `--nos-vst` is specified: * `cpp_db_idle_seconds` time since last database activity (upload/rename/delete) * `cpp_db_act_seconds` same but as an absolute timestamp * `cpp_idle_vols` number of volumes which are idle / ready * `cpp_busy_vols` number of volumes which are busy / indexing * `cpp_offline_vols` number of volumes which are offline / unavailable * `cpp_hashing_files` number of files queued for hashing / indexing * `cpp_tagq_files` number of files queued for metadata scanning * `cpp_mtpq_files` number of files queued for plugin-based analysis and these are available per-volume only: * `cpp_disk_size_bytes` total HDD size * `cpp_disk_free_bytes` free HDD space and these are per-volume and `total`: * `cpp_vol_bytes` size of all files in volume * `cpp_vol_files` number of files * `cpp_dupe_bytes` disk space presumably saved by deduplication * `cpp_dupe_files` number of dupe files * `cpp_unf_bytes` currently unfinished / incoming uploads some of the metrics have additional requirements to function correctly, * `cpp_vol_*` requires either the `e2ds` volflag or `-e2dsa` global-option the following options are available to disable some of the metrics: * `--nos-hdd` disables `cpp_disk_*` which can prevent spinning up HDDs * `--nos-vol` disables `cpp_vol_*` which reduces server startup time * `--nos-vst` disables volume state, reducing the worst-case prometheus query time by 0.5 sec * `--nos-dup` disables `cpp_dupe_*` which reduces the server load caused by prometheus queries * `--nos-unf` disables `cpp_unf_*` for no particular purpose note: the following metrics are counted incorrectly if multiprocessing is enabled with `-j`: `cpp_http_conns`, `cpp_http_reqs`, `cpp_sus_reqs`, `cpp_active_bans`, `cpp_total_bans` ## other extremely specific features you'll never find a use for these: ### custom mimetypes change the association of a file extension using commandline args, you can do something like `--mime gif=image/jif` and `--mime ts=text/x.typescript` (can be specified multiple times) in a config file, this is the same as: ```yaml [global] mime: gif=image/jif mime: ts=text/x.typescript ``` run copyparty with `--mimes` to list all the default mappings ### GDPR compliance imagine using copyparty professionally... **TINLA/IANAL; EU laws are hella confusing** * remember to disable logging, or configure logrotation to an acceptable timeframe with `-lo cpp-%Y-%m%d.txt.xz` or similar * if running with the database enabled (recommended), then have it forget uploader-IPs after some time using `--forget-ip 43200` * don't set it too low; [unposting](#unpost) a file is no longer possible after this takes effect * if you actually *are* a lawyer then I'm open for feedback, would be fun ### feature chickenbits buggy feature? rip it out by setting any of the following environment variables to disable its associated bell or whistle, | env-var | what it does | | -------------------- | ------------ | | `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 | | `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access | | `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes | | `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` | | `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) | | `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads | | `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) | | `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) | | `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy | | `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider | | `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations | example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py` ### feature beefybits force-enable features with known issues on your OS/env by setting any of the following environment variables, also affectionately known as `fuckitbits` or `hail-mary-bits` | 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 the party might be closer than you think if your distro/OS is not mentioned below, there might be some hints in the [«on servers»](#on-servers) section ## arch package `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/)) it comes with a [systemd service](./contrib/systemd/copyparty@.service) as well as a [user service](./contrib/systemd/copyparty-user.service), and expects to find a [config file](./contrib/systemd/copyparty.example.conf) in `/etc/copyparty/copyparty.conf` or `~/.config/copyparty/copyparty.conf` after installing, start either the system service or the user service and navigate to http://127.0.0.1:3923 for further instructions (unless you already edited the config files, in which case you are good to go, probably) > to start the systemd service, either do `systemctl start --user copyparty` to start it as your own user, or `systemctl start copyparty@bob` to use unix-user `bob` ## fedora package does not exist yet; there are rumours that it is being packaged! keep an eye on this space... ## homebrew formulae `brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty should work on all macs (both intel and apple silicon) and all relevant macos versions the homebrew package is maintained by the homebrew team (thanks!) ## nix package `nix profile install github:9001/copyparty` requires a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of nix some recommended dependencies are enabled by default; [override the package](https://github.com/9001/copyparty/blob/hovudstraum/contrib/package/nix/copyparty/default.nix#L3-L22) if you want to add/remove some features/deps `ffmpeg-full` was chosen over `ffmpeg-headless` mainly because we need `withWebp` (and `withOpenmpt` is also nice) and being able to use a cached build felt more important than optimizing for size at the time -- PRs welcome if you disagree 👍 ## nixos module for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS: ```nix { # add copyparty flake to your inputs inputs.copyparty.url = "github:9001/copyparty"; # ensure that copyparty is an allowed argument to the outputs function outputs = { self, nixpkgs, copyparty }: { nixosConfigurations.yourHostName = nixpkgs.lib.nixosSystem { modules = [ # load the copyparty NixOS module copyparty.nixosModules.default ({ pkgs, ... }: { # add the copyparty overlay to expose the package to the module nixpkgs.overlays = [ copyparty.overlays.default ]; # (optional) install the package globally environment.systemPackages = [ pkgs.copyparty ]; # configure the copyparty module services.copyparty.enable = true; }) ]; }; }; } ``` if you don't use a flake in your configuration, you can use other dependency management tools like [npins](https://github.com/andir/npins), [niv](https://github.com/nmattia/niv), or even plain [`fetchTarball`](https://nix.dev/manual/nix/stable/language/builtins#builtins-fetchTarball), like so: ```nix { pkgs, ... }: let # npins example, adjust for your setup. copyparty should be a path to the downloaded repo # for niv, just replace the npins folder import with the sources.nix file copyparty = (import ./npins).copyparty; # or with fetchTarball: copyparty = fetchTarball "https://github.com/9001/copyparty/archive/hovudstraum.tar.gz"; in { # load the copyparty NixOS module imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ]; # add the copyparty overlay to expose the package to the module nixpkgs.overlays = [ (import "${copyparty}/contrib/package/nix/overlay.nix") ]; # (optional) install the package globally environment.systemPackages = [ pkgs.copyparty ]; # configure the copyparty module services.copyparty.enable = true; } ``` copyparty on NixOS is configured via `services.copyparty` options, for example: ```nix services.copyparty = { enable = true; # the user to run the service as user = "copyparty"; # the group to run the service as group = "copyparty"; # directly maps to values in the [global] section of the copyparty config. # see `copyparty --help` for available options settings = { i = "0.0.0.0"; # use lists to set multiple values p = [ 3210 3211 ]; # use booleans to set binary flags no-reload = true; # using 'false' will do nothing and omit the value when generating a config ignored-flag = false; }; # create users accounts = { # specify the account name as the key ed = { # provide the path to a file containing the password, keeping it out of /nix/store # must be readable by the copyparty service user passwordFile = "/run/keys/copyparty/ed_password"; }; # or do both in one go k.passwordFile = "/run/keys/copyparty/k_password"; }; # create a group groups = { # users "ed" and "k" are part of the group g1 g1 = [ "ed" "k" ]; }; # create a volume volumes = { # create a volume at "/" (the webroot), which will "/" = { # share the contents of "/srv/copyparty" path = "/srv/copyparty"; # see `copyparty --help-accounts` for available options access = { # everyone gets read-access, but r = "*"; # users "ed" and "k" get read-write rw = [ "ed" "k" ]; }; # see `copyparty --help-flags` for available options flags = { # "fk" enables filekeys (necessary for upget permission) (4 chars long) fk = 4; # scan for new files every 60sec scan = 60; # volflag "e2d" enables the uploads database e2d = true; # "d2t" disables multimedia parsers (in case the uploads are malicious) d2t = true; # skips hashing file contents if path matches *.iso nohash = "\.iso$"; }; }; }; # you may increase the open file limit for the process openFilesLimit = 8192; }; ``` the passwordFile at /run/keys/copyparty/ could for example be generated by [agenix](https://github.com/ryantm/agenix), or you could just dump it in the nix store instead if that's acceptable # browser support TLDR: yes ![copyparty-ie4-fs8](https://user-images.githubusercontent.com/241032/118192791-fb31fe00-b446-11eb-9647-898ea8efc1f7.png) `ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android | feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr | | --------------- | --- | ---- | ---- | ---- | ----- | ---- | --- | ---- | | browse files | yep | yep | yep | yep | yep | yep | yep | yep | | thumbnail view | - | yep | yep | yep | yep | yep | yep | yep | | basic uploader | yep | yep | yep | yep | yep | yep | yep | yep | | up2k | - | - | `*1` | `*1` | yep | yep | yep | yep | | make directory | yep | yep | yep | yep | yep | yep | yep | yep | | send message | yep | yep | yep | yep | yep | yep | yep | yep | | set sort order | - | yep | yep | yep | yep | yep | yep | yep | | zip selection | - | yep | yep | yep | yep | yep | yep | yep | | file search | - | yep | yep | yep | yep | yep | yep | yep | | file rename | - | yep | yep | yep | yep | yep | yep | yep | | file cut/paste | - | yep | yep | yep | yep | yep | yep | yep | | unpost uploads | - | - | yep | yep | yep | yep | yep | yep | | navpane | - | yep | yep | yep | yep | yep | yep | yep | | image viewer | - | yep | yep | yep | yep | yep | yep | yep | | video player | - | yep | yep | yep | yep | yep | yep | yep | | markdown editor | - | - | `*2` | `*2` | yep | yep | yep | yep | | markdown viewer | - | `*2` | `*2` | `*2` | yep | yep | yep | yep | | play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep | | play ogg/opus | - | - | - | - | yep | yep | `*3` | yep | | **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr | * internet explorer 6 through 8 behave the same * firefox 52 and chrome 49 are the final winxp versions * `*1` yes, but extremely slow (ie10: `1 MiB/s`, ie11: `270 KiB/s`) * `*2` only able to do plaintext documents (no markdown rendering) * `*3` iOS 11 and newer, opus only, and requires FFmpeg on the server quick summary of more eccentric web-browsers trying to view a directory index: | browser | will it blend | | ------- | ------------- | | **links** (2.21/macports) | can browse, login, upload/mkdir/msg | | **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg | | **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg | | **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) | | **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio | | **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` | | **ncsa mosaic** 2.7 | does not get a pass, [pic1](https://user-images.githubusercontent.com/241032/174189227-ae816026-cf6f-4be5-a26e-1b3b072c1b2f.png) - [pic2](https://user-images.githubusercontent.com/241032/174189225-5651c059-5152-46e9-ac26-7e98e497901b.png) | | **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl | | **sony psp** 5.50 | can browse, upload/mkdir/msg (thx dwarf) [screenshot](https://github.com/user-attachments/assets/9d21f020-1110-4652-abeb-6fc09c533d4f) | | **nintendo 3ds** | can browse, upload, view thumbnails (thx bnjmn) | | **Nintendo Wii (Opera 9.0 "Internet Channel")** | can browse, can't upload or download (no local storage), can view images - works best with `?b=u`, default view broken |

# server hall of fame unexpected things that run copyparty: * an old [allwinner](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/aallwinner.jpg) android tv-box (ziptie-strapped to an HDD) running a firmware which flips the CPU into Big-Endian mode early during boot * copyparty is [certified BE ready](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/be-ready.png) -- thanks, [Øl Telecom](http://ol-tele.com/)! * an [SGI O2 (photo)](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.jpg?cache) with a grand total of 64 MiB RAM running SGI IRIX; [screenshot](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/sgi-o2.png?cache) * thanks again to the wonderful people at [Øl Telecom](http://ol-tele.com/) * a [wristwatch](https://a.ocv.me/pub/g/nerd-stuff/cpp/servers/clockyparty.jpg) # client examples interact with copyparty using non-browser clients * javascript: dump some state into a file (two separate examples) * `await fetch('//127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});` * `var xhr = new XMLHttpRequest(); xhr.open('POST', '//127.0.0.1:3923/msgs?raw'); xhr.send('foo');` * curl/wget: upload some files (post=file, chunk=stdin) * `post(){ curl -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}` `post movie.mkv` (gives HTML in return) * `post(){ curl -F f=@"$1" 'http://127.0.0.1:3923/?want=url&pw=wark';}` `post movie.mkv` (gives hotlink in return) * `post(){ curl -H pw:wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}` `post movie.mkv` (randomized filename) * `post(){ wget --header='pw: wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}` `post movie.mkv` * `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}` `chunk /dev/tcp/127.0.0.1/3923` * python: [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) is a command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm) * file uploads, file-search, [folder sync](#folder-sync), autoresume of aborted/broken uploads * can be downloaded from copyparty: controlpanel -> connect -> [u2c.py](http://127.0.0.1:3923/.cpr/a/u2c.py) * see [./bin/README.md#u2cpy](bin/README.md#u2cpy) * FUSE: mount a copyparty server as a local filesystem * cross-platform python client available in [./bin/](bin/) * able to mount nginx and iis directory listings too, not just copyparty * can be downloaded from copyparty: controlpanel -> connect -> [partyfuse.py](http://127.0.0.1:3923/.cpr/a/partyfuse.py) * [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md) * sharex (screenshot utility): see [./contrib/sharex.sxcu](./contrib/#sharexsxcu) * and for screenshots on macos, see [./contrib/ishare.iscu](./contrib/#ishareiscu) * and for screenshots on linux, see [./contrib/flameshot.sh](./contrib/flameshot.sh) * [Custom Uploader](https://f-droid.org/en/packages/com.nyx.custom_uploader/) (an Android app) as an alternative to copyparty's own [PartyUP!](#android-app) * works if you set UploadURL to `https://your.com/foo/?want=url&pw=hunter2` and FormDataName `f` * contextlet (web browser integration); see [contrib contextlet](contrib/#send-to-cppcontextletjson) * [igloo irc](https://iglooirc.com/): Method: `post` Host: `https://you.com/up/?want=url&pw=hunter2` Multipart: `yes` File parameter: `f` copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uploads: b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;} b512 for basic-authentication, all of the following are accepted: `password` / `whatever:password` / `password:whatever` (the username is ignored) * unless you've enabled `--usernames`, then it's `PW: usr:pwd`, cookie `cppwd=usr:pwd`, url-param `?pw=usr:pwd` NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename ## folder sync sync folders to/from copyparty NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty * if you want bidirectional sync, then copyparty and syncthing *should* be entirely safe to combine; they should be able to collaborate on the same folders without causing any trouble for eachother. Many people do this, and there have been no issues so far. But, if you *do* encounter any problems, please [file a copyparty bug](https://github.com/9001/copyparty/issues/new/choose) and I'll try to help -- just keep in mind I've never used syncthing before :-) the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading) if you want to sync with `u2c.py` then: * the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into * ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into * ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a` * quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare * starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections * but this is only true for the initial upload; u2c will be faster for periodic syncing ## mount as drive a remote copyparty server as a local filesystem; go to the control-panel and click `connect` to see a list of commands to do that alternatively, some alternatives roughly sorted by speed (unreproducible benchmark), best first: * [rclone-webdav](./docs/rclone.md) (25s), read/WRITE (rclone v1.63 or later) * [rclone-http](./docs/rclone.md) (26s), read-only * [partyfuse.py](./bin/#partyfusepy) (26s), read-only * [rclone-ftp](./docs/rclone.md) (47s), read/WRITE * davfs2 (103s), read/WRITE * [win10-webdav](#webdav-server) (138s), read/WRITE * [win10-smb2](#smb-server) (387s), read/WRITE most clients will fail to mount the root of a copyparty server unless there is a root volume (so you get the admin-panel instead of a browser when accessing it) -- in that case, mount a specific volume instead if you have volumes that are accessible without a password, then some webdav clients (such as davfs2) require the global-option `--dav-auth` to access any password-protected areas # android app upload to copyparty with one tap Get it on F-Droid '' f-droid version info '' github version info the app is **NOT** the full copyparty server! just a basic upload client, nothing fancy yet if you want to run the copyparty server on your android device, see [install on android](#install-on-android) # iOS shortcuts there is no iPhone app, but the following shortcuts are almost as good: * [upload to copyparty](https://www.icloud.com/shortcuts/41e98dd985cb4d3bb433222bc1e9e770) ([offline](https://github.com/9001/copyparty/raw/hovudstraum/contrib/ios/upload-to-copyparty.shortcut)) ([png](https://user-images.githubusercontent.com/241032/226118053-78623554-b0ed-482e-98e4-6d57ada58ea4.png)) based on the [original](https://www.icloud.com/shortcuts/ab415d5b4de3467b9ce6f151b439a5d7) by [Daedren](https://github.com/Daedren) (thx!) * can strip exif, upload files, pics, vids, links, clipboard * can download links and rehost the target file on copyparty (see first comment inside the shortcut) * pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there if you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS) # performance defaults are usually fine - expect `8 GiB/s` download, `1 GiB/s` upload below are some tweaks roughly ordered by usefulness: * disabling HTTP/2 and HTTP/3 can make uploads 5x faster, depending on server/client software * `-q` disables logging and can help a bunch, even when combined with `-lo` to redirect logs to file * `--hist` pointing to a fast location (ssd) will make directory listings and searches faster when `-e2d` or `-e2t` is set * and also makes thumbnails load faster, regardless of e2d/e2t * `--dedup` enables deduplication and thus avoids writing to the HDD if someone uploads a dupe * `--safe-dedup 1` makes deduplication much faster during upload by skipping verification of file contents; safe if there is no other software editing/moving the files in the volumes * `--no-dirsz` shows the size of folder inodes instead of the total size of the contents, giving about 30% faster folder listings * `--no-hash .` when indexing a network-disk if you don't care about the actual filehashes and only want the names/tags searchable * if your volumes are on a network-disk such as NFS / SMB / s3, specifying larger values for `--iobuf` and/or `--s-rd-sz` and/or `--s-wr-sz` may help; try setting all of them to `524288` or `1048576` or `4194304` * `--no-htp --hash-mt=0 --mtag-mt=1 --th-mt=1` minimizes the number of threads; can help in some eccentric environments (like the vscode debugger) * when running on AlpineLinux or other musl-based distro, try mimalloc for higher performance (and twice as much RAM usage); `apk add mimalloc2` and run copyparty with env-var `LD_PRELOAD=/usr/lib/libmimalloc-secure.so.2` * note that mimalloc requires special care when combined with prisonparty and/or bubbleparty/bubblewrap; you must give it access to `/proc` and `/sys` otherwise you'll encounter issues with FFmpeg (audio transcoding, thumbnails) * `-j0` (usually *not* recommended) enables multiprocessing (actual multithreading), can reduce latency to `20+80/numCores` percent and generally improve performance in cpu-intensive workloads, for example: * lots of connections (many users or heavy clients) * simultaneous downloads and uploads saturating a 20gbps connection * if `-e2d` is enabled, `-j2` gives 4x performance for directory listings; `-j4` gives 16x ...however it will probably *reduce* performance in most cases, since it also increases the server/filesystem/HDD load during uploads, and adds an overhead to internal communication, so keeping the default is generally best * using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others * and pypy can sometimes crash on startup with `-j0` (TODO make issue) * if you are running the copyparty server **on Windows or Macos:** * `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem * this is the same as `casechk: n` in a config-file ## client-side when uploading files, * when uploading from very fast storage (NVMe SSD) with chrome/firefox, enable `[wasm]` in the `[⚙️] settings` tab to more effectively use all CPU-cores for hashing * don't do this on Safari (runs faster without) * don't do this on older browsers; likely to provoke browser-bugs (browser eats all RAM and crashes) * can be made default-enabled serverside with `--nosubtle 137` (chrome v137+) or `--nosubtle 2` (chrome+firefox) * chrome is recommended (unfortunately), at least compared to firefox: * up to 90% faster when hashing, especially on SSDs * up to 40% faster when uploading over extremely fast internets * but [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) can be 40% faster than chrome again * if you're cpu-bottlenecked, or the browser is maxing a cpu core: * up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it) * optionally you can switch to the lightweight potato ui by clicking the `[🥔]` * switching to another browser-tab also works, the favicon will update every 10 seconds in that case * unlikely to be a problem, but can happen when uploading many small files, or your internet is too fast, or PC too slow # security there is a [discord server](https://discord.gg/25J8CdTT6G) with announcements ; an `@everyone` for all important updates (at the lack of better ideas) some notes on hardening * set `--rproxy 0` *if and only if* your copyparty is directly facing the internet (not through a reverse-proxy) * cors doesn't work right otherwise * if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml` * this returns html documents and svg images as plaintext, and also disables markdown rendering * the `nohtml` volflag also enables `noscript` which, on its own, prevents *most* javascript from running; enabling just `noscript` without `nohtml` makes it probably-safe (see below) to view html and svg files, but `nohtml` is necessary to block javascript in markdown documents * "probably-safe" because it relies on `Content-Security-Policy` so it depends on the reverseproxy to forward it, and the browser to understand it, but `nohtml` (the nuclear option) always works * when running behind a reverse-proxy, listen on a unix-socket for tighter access control (and more performance); see [reverse-proxy](#reverse-proxy) or [`--help-bind`](https://copyparty.eu/cli/#bind-help-page) safety profiles: * option `-s` is a shortcut to set the following options: * `--no-thumb` disables thumbnails and audio transcoding to stop copyparty from running `FFmpeg`/`Pillow`/`VIPS` on uploaded files, which is a [good idea](https://www.cvedetails.com/vulnerability-list.php?vendor_id=3611) if anonymous upload is enabled * `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate * `--dotpart` hides uploads from directory listings while they're still incoming * `--no-robots` and `--force-js` makes life harder for crawlers, see [hiding from google](#hiding-from-google) * option `-ss` is a shortcut for the above plus: * `--unpost 0`, `--no-del`, `--no-mv` disables all move/delete support * `--hardlink` creates hardlinks instead of symlinks when deduplicating uploads, which is less maintenance * however note if you edit one file it will also affect the other copies * `--vague-403` returns a "404 not found" instead of "401 unauthorized" which is a common enterprise meme * `-nih` removes the server hostname from directory listings * option `-sss` is a shortcut for the above plus: * `--no-dav` disables webdav support * `--no-logues` and `--no-readme` disables support for readme's and prologues / epilogues in directory listings, which otherwise lets people upload arbitrary (but sandboxed) ` ================================================ FILE: contrib/ishare.iscu ================================================ { "Name": "copyparty", "RequestURL": "http://127.0.0.1:3923/screenshots/", "Headers": { "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE", "accept": "json" }, "FileFormName": "f", "ResponseURL": "{{fileurl}}" } ================================================ FILE: contrib/lighttpd/subdomain.conf ================================================ # example usage for benchmarking: # # taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subdomain.conf # # lighttpd can connect to copyparty using either tcp (127.0.0.1) # or a unix-socket, but unix-sockets are 37% faster because # lighttpd doesn't reuse tcp connections, so we're doing unix-sockets # # this means we must run copyparty with one of the following: # # -i unix:777:/dev/shm/party.sock # -i unix:777:/dev/shm/party.sock,127.0.0.1 # # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 server.port = 80 server.document-root = "/var/empty" server.upload-dirs = ( "/dev/shm", "/tmp" ) server.modules = ( "mod_proxy" ) proxy.forwarded = ( "for" => 1, "proto" => 1 ) proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) ) # if you really need to use tcp instead of unix-sockets, do this instead: #proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) ) ================================================ FILE: contrib/lighttpd/subpath.conf ================================================ # example usage for benchmarking: # # taskset -c 1 lighttpd -Df ~/dev/copyparty/contrib/lighttpd/subpath.conf # # lighttpd can connect to copyparty using either tcp (127.0.0.1) # or a unix-socket, but unix-sockets are 37% faster because # lighttpd doesn't reuse tcp connections, so we're doing unix-sockets # # this means we must run copyparty with one of the following: # # -i unix:777:/dev/shm/party.sock # -i unix:777:/dev/shm/party.sock,127.0.0.1 # # also since this example proxies a subpath instead of the # recommended subdomain-proxying, we must also specify this: # # --rp-loc files # # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 server.port = 80 server.document-root = "/var/empty" server.upload-dirs = ( "/dev/shm", "/tmp" ) server.modules = ( "mod_proxy" ) $HTTP["url"] =~ "^/files" { proxy.forwarded = ( "for" => 1, "proto" => 1 ) proxy.server = ( "" => ( ( "host" => "/dev/shm/party.sock" ) ) ) # if you really need to use tcp instead of unix-sockets, do this instead: #proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => "3923" ) ) ) } ================================================ FILE: contrib/media-osd-bgone.ps1 ================================================ # media-osd-bgone.ps1: disable media-control OSD on win10do # v1.1, 2021-06-25, ed , MIT-licensed # https://github.com/9001/copyparty/blob/hovudstraum/contrib/media-osd-bgone.ps1 # # locates the first window that looks like the media OSD and minimizes it; # doing this once after each reboot should do the trick # (adjust the width/height filter if it doesn't work) # # --------------------------------------------------------------------- # # tip: save the following as "media-osd-bgone.bat" next to this script: # start cmd /c "powershell -command ""set-executionpolicy -scope process bypass; .\media-osd-bgone.ps1"" & ping -n 2 127.1 >nul" # # then create a shortcut to that bat-file and move the shortcut here: # %appdata%\Microsoft\Windows\Start Menu\Programs\Startup # # and now this will autorun on bootup Add-Type -TypeDefinition @" using System; using System.IO; using System.Threading; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Forms; namespace A { public class B : Control { [DllImport("user32.dll")] static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo); [DllImport("user32.dll", SetLastError = true)] static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll", SetLastError=true)] static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int x; public int y; public int x2; public int y2; } bool fa() { RECT r; IntPtr it = IntPtr.Zero; while ((it = FindWindowEx(IntPtr.Zero, it, "NativeHWNDHost", "")) != IntPtr.Zero) { if (FindWindowEx(it, IntPtr.Zero, "DirectUIHWND", "") == IntPtr.Zero) continue; if (!GetWindowRect(it, out r)) continue; int w = r.x2 - r.x + 1; int h = r.y2 - r.y + 1; Console.WriteLine("[*] hwnd {0:x} @ {1}x{2} sz {3}x{4}", it, r.x, r.y, w, h); if (h != 141) continue; ShowWindow(it, 6); Console.WriteLine("[+] poof"); return true; } return false; } void fb() { keybd_event((byte)Keys.VolumeMute, 0, 0, 0); keybd_event((byte)Keys.VolumeMute, 0, 2, 0); Thread.Sleep(500); keybd_event((byte)Keys.VolumeMute, 0, 0, 0); keybd_event((byte)Keys.VolumeMute, 0, 2, 0); while (true) { if (fa()) { break; } Console.WriteLine("[!] not found"); Thread.Sleep(1000); } this.Invoke((MethodInvoker)delegate { Application.Exit(); }); } public void Run() { Console.WriteLine("[+] hi"); new Thread(new ThreadStart(fb)).Start(); Application.Run(); Console.WriteLine("[+] bye"); } } } "@ -ReferencedAssemblies System.Windows.Forms (New-Object -TypeName A.B).Run() ================================================ FILE: contrib/nginx/copyparty.conf ================================================ # look for "max clients:" when starting copyparty, as nginx should # not accept more consecutive clients than what copyparty is able to; # nginx default is 512 (worker_processes 1, worker_connections 512) # # ====================================================================== # # to reverse-proxy a specific path/subpath/location below a domain # (rather than a complete subdomain), for example "/qw/er", you must # run copyparty with --rp-loc /qw/er and also change the following: # location / { # proxy_pass http://cpp_tcp; # to this: # location /qw/er/ { # proxy_pass http://cpp_tcp/qw/er/; # # ====================================================================== # # rarely, in some extreme usecases, it can be good to add -j0 # (40'000 requests per second, or 20gbps upload/download in parallel) # but this is usually counterproductive and slightly buggy # # ====================================================================== # # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 # # ====================================================================== # # if you are behind cloudflare (or another CDN/WAF/protection service), # remember to reject all connections which are not coming from your # protection service -- for cloudflare in particular, you can # generate the list of permitted IP ranges like so: # (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf # # and then enable it below by uncommenting the cloudflare-only.conf line # # ====================================================================== upstream cpp_tcp { # alternative 1: connect to copyparty using tcp; # cpp_uds is slightly faster and more secure, but # cpp_tcp is easier to setup and "just works" # ...you should however restrict copyparty to only # accept connections from nginx by adding these args: # -i 127.0.0.1 server 127.0.0.1:3923 fail_timeout=1s; keepalive 1; } upstream cpp_uds { # alternative 2: unix-socket, aka. "unix domain socket"; # 5-10% faster, and better isolation from other software, # but there must be at least one unix-group which both # nginx and copyparty is a member of; if that group is # "www" then run copyparty with the following args: # -i unix:770:www:/dev/shm/party.sock server unix:/dev/shm/party.sock fail_timeout=1s; keepalive 1; } server { listen 443 ssl; listen [::]:443 ssl; server_name fs.example.com; # uncomment the following line to reject non-cloudflare connections, ensuring client IPs cannot be spoofed: #include /etc/nginx/cloudflare-only.conf; location / { # recommendation: replace cpp_tcp with cpp_uds below proxy_pass http://cpp_tcp; proxy_redirect off; # disable buffering (next 4 lines) proxy_http_version 1.1; client_max_body_size 0; proxy_buffering off; proxy_request_buffering off; # improve download speed from 600 to 1500 MiB/s proxy_buffers 32 8k; proxy_buffer_size 16k; proxy_busy_buffers_size 24k; proxy_set_header Connection "Keep-Alive"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # NOTE: with cloudflare you want this X-Forwarded-For instead: #proxy_set_header X-Forwarded-For $http_cf_connecting_ip; } } # default client_max_body_size (1M) blocks uploads larger than 256 MiB client_max_body_size 1024M; client_header_timeout 610m; client_body_timeout 610m; send_timeout 610m; ================================================ FILE: contrib/nixos/modules/copyparty.nix ================================================ { config, pkgs, lib, ... }: with lib; let mkKeyValue = key: value: if value == true then # sets with a true boolean value are coerced to just the key name key else if value == false then # or omitted completely when false "" else (generators.mkKeyValueDefault { inherit mkValueString; } ": " key value); mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value); mkValueString = value: if isList value then (concatStringsSep "," (map mkValueString value)) else if isAttrs value then "\n" + (mkAttrsString value) else (generators.mkValueStringDefault { } value); mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]"; mkSection = name: attrs: '' ${mkSectionName name} ${mkAttrsString attrs} ''; mkVolume = name: attrs: '' ${mkSectionName name} ${attrs.path} ${mkAttrsString { accs = attrs.access; flags = attrs.flags; }} ''; passwordPlaceholder = name: "{{password-${name}}}"; accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name); volumesWithoutVariables = filterAttrs (k: v: !(hasInfix "\${" v.path)) cfg.volumes; configStr = '' ${mkSection "global" cfg.settings} ${cfg.globalExtraConfig} ${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)} ${mkSection "groups" cfg.groups} ${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)} ''; cfg = config.services.copyparty; configFile = pkgs.writeText "copyparty.conf" configStr; runtimeConfigPath = "/run/copyparty/copyparty.conf"; externalCacheDir = "/var/cache/copyparty"; externalStateDir = "/var/lib/copyparty"; defaultShareDir = "${externalStateDir}/data"; in { options.services.copyparty = { enable = mkEnableOption "web-based file manager"; package = mkPackageOption pkgs "copyparty" { extraDescription = '' Package of the application to run, exposed for overriding purposes. ''; }; mkHashWrapper = mkOption { type = types.bool; default = true; description = '' Make a shell script wrapper called {command}`copyparty-hash` with all options set here, that launches the hashing cli. ''; }; user = mkOption { type = types.str; default = "copyparty"; description = '' The user that copyparty will run under. If changed from default, you are responsible for making sure the user exists. ''; }; group = mkOption { type = types.str; default = "copyparty"; description = '' The group that copyparty will run under. If changed from default, you are responsible for making sure the user exists. ''; }; openFilesLimit = mkOption { default = 4096; type = types.either types.int types.str; description = "Number of files to allow copyparty to open."; }; settings = mkOption { type = types.attrs; description = '' Global settings to apply. Directly maps to values in the `[global]` section of the copyparty config. Cannot set "c" or "hist", those are set by this module. See {command}`copyparty --help` for more details. ''; default = { i = "127.0.0.1"; no-reload = true; hist = externalCacheDir; }; example = literalExpression '' { i = "0.0.0.0"; no-reload = true; hist = ${externalCacheDir}; } ''; }; globalExtraConfig = mkOption { type = types.str; default = ""; description = "Appended to the end of the `[global]` section verbatim. This is useful for flags which are used in a repeating manner (e.g. `ipu: 255.255.255.1=user`) which can't be repeated in the settings = {} attribute set."; }; accounts = mkOption { type = types.attrsOf ( types.submodule ( { ... }: { options = { passwordFile = mkOption { type = types.str; description = '' Runtime file path to a file containing the user password. Must be readable by the copyparty user. ''; example = "/run/keys/copyparty/ed"; }; }; } ) ); description = '' A set of copyparty accounts to create. ''; default = { }; example = literalExpression '' { ed.passwordFile = "/run/keys/copyparty/ed"; }; ''; }; groups = mkOption { type = types.attrsOf (types.listOf types.str); description = '' A set of copyparty groups to create and the users that should be part of each group. ''; default = { }; example = literalExpression '' { group_name = [ "user1" "user2" ]; }; ''; }; volumes = mkOption { type = types.attrsOf ( types.submodule ( { ... }: { options = { path = mkOption { type = types.path; description = '' Path of a directory to share. ''; }; access = mkOption { type = types.attrs; description = '' Attribute list of permissions and the users to apply them to. The key must be a string containing any combination of allowed permission: * "r" (read): list folder contents, download files * "w" (write): upload files; need "r" to see the uploads * "m" (move): move files and folders; need "w" at destination * "d" (delete): permanently delete files and folders * "g" (get): download files, but cannot see folder contents * "G" (upget): "get", but can see filekeys of their own uploads * "h" (html): "get", but folders return their index.html * "a" (admin): can see uploader IPs, config-reload For example: "rwmd" The value must be one of: * an account name, defined in `accounts` * a list of account names * "*", which means "any account" ''; example = literalExpression '' { # wG = write-upget = see your own uploads only wG = "*"; # read-write-modify-delete for users "ed" and "k" rwmd = ["ed" "k"]; }; ''; }; flags = mkOption { type = types.attrs; description = '' Attribute list of volume flags to apply. See {command}`copyparty --help-flags` for more details. ''; example = literalExpression '' { # "fk" enables filekeys (necessary for upget permission) (4 chars long) fk = 4; # scan for new files every 60sec scan = 60; # volflag "e2d" enables the uploads database e2d = true; # "d2t" disables multimedia parsers (in case the uploads are malicious) d2t = true; # skips hashing file contents if path matches *.iso nohash = "\.iso$"; }; ''; default = { }; }; }; } ) ); description = "A set of copyparty volumes to create"; default = { "/" = { path = defaultShareDir; access = { r = "*"; }; }; }; example = literalExpression '' { "/" = { path = ${defaultShareDir}; access = { # wG = write-upget = see your own uploads only wG = "*"; # read-write-modify-delete for users "ed" and "k" rwmd = ["ed" "k"]; }; }; }; ''; }; }; config = mkIf cfg.enable ( let command = "${getExe cfg.package} -c ${runtimeConfigPath}"; in { systemd.services.copyparty = { description = "http file sharing hub"; wantedBy = [ "multi-user.target" ]; environment = { PYTHONUNBUFFERED = "true"; XDG_CONFIG_HOME = externalStateDir; }; preStart = let replaceSecretCommand = name: attrs: "${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}"; in '' set -euo pipefail install -m 600 ${configFile} ${runtimeConfigPath} ${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)} ''; serviceConfig = { Type = "simple"; ExecStart = command; # Hardening options User = cfg.user; Group = cfg.group; RuntimeDirectory = [ "copyparty" ]; RuntimeDirectoryMode = "0700"; StateDirectory = [ "copyparty" ]; StateDirectoryMode = "0700"; CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ]; CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700"; WorkingDirectory = externalStateDir; BindReadOnlyPaths = [ "/nix/store" "-/etc/resolv.conf" "-/etc/nsswitch.conf" "-/etc/group" "-/etc/hosts" "-/etc/localtime" ] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts); BindPaths = (if cfg.settings ? hist then [ cfg.settings.hist ] else [ ]) ++ [ externalStateDir ] ++ (mapAttrsToList (k: v: v.path) volumesWithoutVariables); # ProtectSystem = "strict"; # Note that unlike what 'ro' implies, # this actually makes it impossible to read anything in the root FS, # except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`. # This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible. TemporaryFileSystem = "/:ro"; PrivateTmp = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectControlGroups = true; RestrictSUIDSGID = true; PrivateMounts = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectHostname = true; ProtectClock = true; ProtectProc = "invisible"; ProcSubset = "pid"; RestrictNamespaces = true; RemoveIPC = true; UMask = "0077"; LimitNOFILE = cfg.openFilesLimit; NoNewPrivileges = true; LockPersonality = true; RestrictRealtime = true; MemoryDenyWriteExecute = true; }; }; # ensure volumes exist: systemd.tmpfiles.settings."copyparty" = ( lib.attrsets.mapAttrs' ( name: value: lib.attrsets.nameValuePair (value.path) { d = { #: in front of things means it wont change it if the directory already exists. group = ":${cfg.group}"; user = ":${cfg.user}"; mode = ":${ # Use volume permissions if set if (value.flags ? chmod_d) then value.flags.chmod_d # Else, use global permission if set else if (cfg.settings ? chmod-d) then cfg.settings.chmod-d # Else, use the default permission else "755" }"; }; } ) volumesWithoutVariables ); users.groups = lib.mkIf (cfg.group == "copyparty") { copyparty = { }; }; users.users = lib.mkIf (cfg.user == "copyparty") { copyparty = { description = "Service user for copyparty"; group = cfg.group; home = externalStateDir; isSystemUser = true; }; }; environment.systemPackages = lib.mkIf cfg.mkHashWrapper [ (pkgs.writeShellScriptBin "copyparty-hash" '' set -a # automatically export variables # set same environment variables as the systemd service ${lib.pipe config.systemd.services.copyparty.environment [ (lib.filterAttrs (n: v: v != null && n != "PATH")) (lib.mapAttrs (_: v: "${v}")) (lib.toShellVars) ]} PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH exec ${command} --ah-cli '') ]; } ); } ================================================ FILE: contrib/openrc/copyparty ================================================ #!/sbin/openrc-run # this will start `/usr/local/bin/copyparty-sfx.py` # and share '/mnt' with anonymous read+write # # installation: # cp -pv copyparty /etc/init.d && rc-update add copyparty # # you may want to: # change '/usr/bin/python' to another interpreter # change '/mnt::rw' to another location or permission-set # use a config file instead of command arguments, e.g.: # command_args="-c /etc/copyparty.conf" name="$SVCNAME" command_background=true extra_commands="checkconfig" pidfile="/var/run/$SVCNAME.pid" command="/usr/bin/python3 /usr/local/bin/copyparty-sfx.py" command_args="-q -v /mnt::rw" checkconfig() { ebegin "Checking $RC_SVCNAME configuration" $command $command_args --exit cfg eend $? } ================================================ FILE: contrib/package/arch/PKGBUILD ================================================ # Maintainer: icxes # Contributor: Morgan Adamiec # NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead. pkgname=copyparty pkgver="1.20.12" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") url="https://github.com/9001/${pkgname}" license=('MIT') depends=("bash" "python" "lsof" "python-jinja") makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz") optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags" "cfssl: generate TLS certificates on startup" "python-mutagen: music tags (alternative)" "python-paramiko: sftp server", "python-pillow: thumbnails for images" "python-pyvips: thumbnails for images (higher quality, faster, uses more ram)" "libkeyfinder: detection of musical keys" "python-pyopenssl: ftps functionality" "python-pyzmq: send zeromq messages from event-hooks" "python-argon2-cffi: hashed passwords in config" ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("etc/${pkgname}/copyparty.conf" ) sha256sums=("3acb98e9c577245517db92e77519caeac8a3e382dbc8704ec4a8701c7d58ba76") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" make cd "${srcdir}/${pkgname}-${pkgver}" python -m build --wheel --no-isolation } package() { cd "${srcdir}/${pkgname}-${pkgver}" python -m installer --destdir="$pkgdir" dist/*.whl install -dm755 "${pkgdir}/etc/${pkgname}" install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty" install -Dm644 "contrib/systemd/${pkgname}.conf" "${pkgdir}/etc/${pkgname}/copyparty.conf" install -Dm644 "contrib/systemd/${pkgname}@.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}@.service" install -Dm644 "contrib/systemd/${pkgname}-user.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service" install -Dm644 "contrib/systemd/prisonparty@.service" "${pkgdir}/usr/lib/systemd/system/prisonparty@.service" install -Dm644 "contrib/systemd/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md" install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } ================================================ FILE: contrib/package/makedeb-mpr/PKGBUILD ================================================ # Contributor: Beethoven pkgname=copyparty pkgver=1.20.12 pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") url="https://github.com/9001/${pkgname}" license=('MIT') depends=("bash" "python3" "lsof" "python3-jinja2") makedepends=("python3-wheel" "python3-setuptools" "python3-build" "python3-installer" "make" "pigz") optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags" "golang-cfssl: generate TLS certificates on startup" "python3-mutagen: music tags (alternative)" "python3-paramiko: sftp server" "python3-pil: thumbnails for images" "python3-openssl: ftps functionality" "python3-zmq: send zeromq messages from event-hooks" "python3-argon2: hashed passwords in config" ) source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") backup=("/etc/${pkgname}.d/init" ) sha256sums=("3acb98e9c577245517db92e77519caeac8a3e382dbc8704ec4a8701c7d58ba76") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" make cd "${srcdir}/${pkgname}-${pkgver}" python -m build --wheel --no-isolation } package() { cd "${srcdir}/${pkgname}-${pkgver}" python -m installer --destdir="$pkgdir" dist/*.whl install -dm755 "${pkgdir}/etc/${pkgname}.d" install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty" install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init" install -Dm644 "contrib/package/makedeb-mpr/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service" install -Dm644 "contrib/package/makedeb-mpr/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service" install -Dm644 "contrib/package/makedeb-mpr/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md" install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } ================================================ FILE: contrib/package/makedeb-mpr/copyparty.conf ================================================ ## import all *.conf files from the current folder (/etc/copyparty.d) % ./ # add additional .conf files to this folder; # see example config files for reference: # https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf # https://github.com/9001/copyparty/tree/hovudstraum/docs/copyparty.d ================================================ FILE: contrib/package/makedeb-mpr/copyparty.service ================================================ # this will start `/usr/bin/copyparty-sfx.py` # and read config from `/etc/copyparty.d/*.conf` # # you probably want to: # change "User=cpp" and "/home/cpp/" to another user # # unless you add -q to disable logging, you may want to remove the # following line to allow buffering (slightly better performance): # Environment=PYTHONUNBUFFERED=x [Unit] Description=copyparty file server [Service] Type=notify SyslogIdentifier=copyparty Environment=PYTHONUNBUFFERED=x WorkingDirectory=/var/lib/copyparty-jail ExecReload=/bin/kill -s USR1 $MAINPID # user to run as + where the TLS certificate is (if any) User=cpp Environment=XDG_CONFIG_HOME=/home/cpp/.config # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' # run copyparty ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init [Install] WantedBy=multi-user.target ================================================ FILE: contrib/package/makedeb-mpr/index.md ================================================ this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured please add some `*.conf` files to `/etc/copyparty.d/` ================================================ FILE: contrib/package/makedeb-mpr/prisonparty.service ================================================ # this will start `/usr/bin/copyparty-sfx.py` # in a chroot, preventing accidental access elsewhere, # and read copyparty config from `/etc/copyparty.d/*.conf` # # expose additional filesystem locations to copyparty # by listing them between the last `cpp` and `--` # # `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000) # # unless you add -q to disable logging, you may want to remove the # following line to allow buffering (slightly better performance): # Environment=PYTHONUNBUFFERED=x [Unit] Description=copyparty file server [Service] SyslogIdentifier=prisonparty Environment=PYTHONUNBUFFERED=x WorkingDirectory=/var/lib/copyparty-jail ExecReload=/bin/kill -s USR1 $MAINPID # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' # run copyparty ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail cpp cpp \ /etc/copyparty.d \ -- \ /usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init [Install] WantedBy=multi-user.target ================================================ FILE: contrib/package/nix/copyparty/default.nix ================================================ { lib, buildPythonApplication, fetchurl, util-linux, python, setuptools, jinja2, impacket, pyopenssl, cfssl, argon2-cffi, pillow, pyvips, pyzmq, ffmpeg, mutagen, paramiko, pyftpdlib, magic, partftpy, fusepy, # for partyfuse # use argon2id-hashed passwords in config files (sha2 is always available) withHashedPasswords ? true, # generate TLS certificates on startup (pointless when reverse-proxied) withCertgen ? false, # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing withThumbnails ? true, # create thumbnails with PyVIPS; even faster, uses more memory # -- can be combined with Pillow to support more filetypes withFastThumbnails ? false, # enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus # -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface # -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both withMediaProcessing ? true, # if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster) withBasicAudioMetadata ? false, # send ZeroMQ messages from event-hooks withZeroMQ ? true, # enable SFTP server withSFTP ? false, # enable FTP server withFTP ? true, # enable FTPS support in the FTP server withFTPS ? false, # enable TFTP server withTFTP ? false, # samba/cifs server; dangerous and buggy, enable if you really need it withSMB ? false, # enables filetype detection for nameless uploads withMagic ? false, # extra packages to add to the PATH extraPackages ? [ ], # function that accepts a python packageset and returns a list of packages to # be added to the python venv. useful for scripts and such that require # additional dependencies extraPythonPackages ? (_p: [ ]), # to build stable + unstable with the same file stable ? true, # for commit date, only used when stable = false copypartyFlake ? null, nix-gitignore, }: let pinData = lib.importJSON ./pin.json; runtimeDeps = ([ util-linux ] ++ extraPackages ++ lib.optional withMediaProcessing ffmpeg); inherit (copypartyFlake) lastModifiedDate; # ex: "1970" "01" "01" dateStringsZeroPrefixed = { year = builtins.substring 0 4 lastModifiedDate; month = builtins.substring 4 2 lastModifiedDate; day = builtins.substring 6 2 lastModifiedDate; }; # ex: "1970" "1" "1" dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed; unstableVersion = if copypartyFlake == null then "${pinData.version}-unstable" else with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}" ; version = if stable then pinData.version else unstableVersion; stableSrc = fetchurl { inherit (pinData) url hash; }; root = ../../../..; unstableSrc = nix-gitignore.gitignoreSource [] root; src = if stable then stableSrc else unstableSrc; rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown"; unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}"); in buildPythonApplication { pname = "copyparty"; inherit version src; postPatch = lib.optionalString (!stable) '' old_src="$(mktemp -d)" tar -C "$old_src" -xf ${stableSrc} declare -a folders folders=("$old_src"/*) count_folders="''${#folders[@]}" if [[ $count_folders != 1 ]]; then declare -p folders echo "Expected 1 folder, found $count_folders" >&2 exit 1 fi old_src_folder="''${folders[0]}" cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py ${lib.optionalString (copypartyFlake != null) (with dateStringsShort; '' sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py '')} ''; dependencies = [ jinja2 fusepy ] ++ lib.optional withSMB impacket ++ lib.optional withSFTP paramiko ++ lib.optional withFTP pyftpdlib ++ lib.optional withFTPS pyopenssl ++ lib.optional withTFTP partftpy ++ lib.optional withCertgen cfssl ++ lib.optional withThumbnails pillow ++ lib.optional withFastThumbnails pyvips ++ lib.optional withMediaProcessing ffmpeg ++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withHashedPasswords argon2-cffi ++ lib.optional withZeroMQ pyzmq ++ lib.optional withMagic magic ++ (extraPythonPackages python.pkgs); makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ]; pyproject = true; build-system = [ setuptools ]; meta = { description = "Turn almost any device into a file server"; longDescription = '' Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps ''; homepage = "https://github.com/9001/copyparty"; changelog = "https://github.com/9001/copyparty/releases/tag/v${pinData.version}"; license = lib.licenses.mit; mainProgram = "copyparty"; sourceProvenance = [ lib.sourceTypes.fromSource ]; }; } ================================================ FILE: contrib/package/nix/copyparty/pin.json ================================================ { "url": "https://github.com/9001/copyparty/releases/download/v1.20.12/copyparty-1.20.12.tar.gz", "version": "1.20.12", "hash": "sha256-OsuY6cV3JFUX25LndRnK6sij44LbyHBOxKhwHH1YunY=" } ================================================ FILE: contrib/package/nix/copyparty/update.py ================================================ #!/usr/bin/env python3 # Update the Nix package pin # # Usage: ./update.sh [PATH] # When the [PATH] is not set, it will fetch the latest release from the repo. # With [PATH] set, it will hash the given file and generate the URL, # base on the version contained within the file import base64 import json import hashlib import sys import tarfile from pathlib import Path OUTPUT_FILE = Path("pin.json") TARGET_ASSET = lambda version: f"copyparty-{version}.tar.gz" HASH_TYPE = "sha256" LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest" DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET(version)}" def get_formatted_hash(binary): hasher = hashlib.new("sha256") hasher.update(binary) asset_hash = hasher.digest() encoded_hash = base64.b64encode(asset_hash).decode("ascii") return f"{HASH_TYPE}-{encoded_hash}" def version_from_tar_gz(path): with tarfile.open(path) as tarball: release_name = tarball.getmembers()[0].name prefix = "copyparty-" if release_name.startswith(prefix): return release_name.replace(prefix, "") raise ValueError("version not found in provided file") def remote_release_pin(): import requests response = requests.get(LATEST_RELEASE_URL).json() version = response["tag_name"].lstrip("v") asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0] download_url = asset_info["browser_download_url"] asset = requests.get(download_url) formatted_hash = get_formatted_hash(asset.content) result = {"url": download_url, "version": version, "hash": formatted_hash} return result def local_release_pin(path): version = version_from_tar_gz(path) download_url = DOWNLOAD_URL(version) formatted_hash = get_formatted_hash(path.read_bytes()) result = {"url": download_url, "version": version, "hash": formatted_hash} return result def main(): if len(sys.argv) > 1: asset_path = Path(sys.argv[1]) result = local_release_pin(asset_path) else: result = remote_release_pin() print(result) json_result = json.dumps(result, indent=4) OUTPUT_FILE.write_text(json_result) if __name__ == "__main__": main() ================================================ FILE: contrib/package/nix/overlay.nix ================================================ final: prev: let fullAttrs = { withHashedPasswords = true; withCertgen = true; withThumbnails = true; withFastThumbnails = true; withMediaProcessing = true; withBasicAudioMetadata = true; withZeroMQ = true; withSFTP = true; withFTP = true; withFTPS = true; withTFTP = true; withSMB = true; withMagic = true; }; call = attrs: final.python3.pkgs.callPackage ./copyparty ({ ffmpeg = final.ffmpeg-full; } // attrs); in { copyparty = call { stable = true; }; copyparty-unstable = call { stable = false; }; copyparty-full = call (fullAttrs // { stable = true; }); copyparty-unstable-full = call (fullAttrs // { stable = false; }); python3 = prev.python3.override { packageOverrides = pyFinal: pyPrev: { partftpy = pyFinal.callPackage ./partftpy { }; }; }; } ================================================ FILE: contrib/package/nix/partftpy/default.nix ================================================ { lib, buildPythonPackage, fetchurl, setuptools, }: let pinData = lib.importJSON ./pin.json; in buildPythonPackage rec { pname = "partftpy"; inherit (pinData) version; pyproject = true; src = fetchurl { inherit (pinData) url hash; }; build-system = [ setuptools ]; pythonImportsCheck = [ "partftpy.TftpServer" ]; meta = { description = "Pure Python TFTP library (copyparty edition)"; homepage = "https://github.com/9001/partftpy"; changelog = "https://github.com/9001/partftpy/releases/tag/${version}"; license = lib.licenses.mit; }; } ================================================ FILE: contrib/package/nix/partftpy/pin.json ================================================ { "url": "https://github.com/9001/partftpy/releases/download/v0.4.0/partftpy-0.4.0.tar.gz", "version": "0.4.0", "hash": "sha256-5Q2zyuJ892PGZmb+YXg0ZPW/DK8RDL1uE0j5HPd4We0=" } ================================================ FILE: contrib/package/nix/partftpy/update.py ================================================ #!/usr/bin/env python3 # Update the Nix package pin # # Usage: ./update.sh import base64 import json import hashlib import sys from pathlib import Path OUTPUT_FILE = Path("pin.json") TARGET_ASSET = lambda version: f"partftpy-{version}.tar.gz" HASH_TYPE = "sha256" LATEST_RELEASE_URL = "https://api.github.com/repos/9001/partftpy/releases/latest" def get_formatted_hash(binary): hasher = hashlib.new("sha256") hasher.update(binary) asset_hash = hasher.digest() encoded_hash = base64.b64encode(asset_hash).decode("ascii") return f"{HASH_TYPE}-{encoded_hash}" def remote_release_pin(): import requests response = requests.get(LATEST_RELEASE_URL).json() version = response["tag_name"].lstrip("v") asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET(version)][0] download_url = asset_info["browser_download_url"] asset = requests.get(download_url) formatted_hash = get_formatted_hash(asset.content) result = {"url": download_url, "version": version, "hash": formatted_hash} return result def main(): result = remote_release_pin() print(result) json_result = json.dumps(result, indent=4) OUTPUT_FILE.write_text(json_result) if __name__ == "__main__": main() ================================================ FILE: contrib/package/rpm/copyparty.spec ================================================ Name: copyparty Version: $pkgver Release: $pkgrel License: MIT Group: Utilities URL: https://github.com/9001/copyparty Source0: copyparty-$pkgver.tar.gz Summary: File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++ BuildArch: noarch BuildRequires: python3, python3-devel, pyproject-rpm-macros, python-setuptools, python-wheel, make Requires: python3, (python3-jinja2 or python-jinja2), lsof Recommends: ffmpeg, (golang-github-cloudflare-cfssl or cfssl), python-mutagen, python-pillow, python-pyvips Recommends: qm-vamp-plugins, python-argon2-cffi, (python-pyopenssl or pyopenssl), python-paramiko, python-impacket %description Portable file server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps See release at https://github.com/9001/copyparty/releases %global debug_package %{nil} %generate_buildrequires %pyproject_buildrequires %prep %setup -q %build cd "copyparty/web" make cd - %pyproject_wheel %install mkdir -p %{buildroot}%{_bindir} mkdir -p %{buildroot}%{_libdir}/systemd/{system,user} mkdir -p %{buildroot}/etc/%{name} mkdir -p %{buildroot}/var/lib/%{name}-jail mkdir -p %{buildroot}%{_datadir}/licenses/%{name} %pyproject_install %pyproject_save_files copyparty install -m 0755 bin/prisonparty.sh %{buildroot}%{_bindir}/prisonpary.sh install -m 0644 contrib/systemd/%{name}.conf %{buildroot}/etc/%{name}/%{name}.conf install -m 0644 contrib/systemd/%{name}@.service %{buildroot}%{_libdir}/systemd/system/%{name}@.service install -m 0644 contrib/systemd/%{name}-user.service %{buildroot}%{_libdir}/systemd/user/%{name}.service install -m 0644 contrib/systemd/prisonparty@.service %{buildroot}%{_libdir}/systemd/system/prisonparty@.service install -m 0644 contrib/systemd/index.md %{buildroot}/var/lib/%{name}-jail/README.md install -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE %files -n copyparty -f %{pyproject_files} %license LICENSE %{_bindir}/copyparty %{_bindir}/partyfuse %{_bindir}/u2c %{_bindir}/prisonpary.sh /etc/%{name}/%{name}.conf %{_libdir}/systemd/system/%{name}@.service %{_libdir}/systemd/user/%{name}.service %{_libdir}/systemd/system/prisonparty@.service /var/lib/%{name}-jail/README.md ================================================ FILE: contrib/plugins/README.md ================================================ # example resource files can be provided to copyparty to tweak things ## example `.epilogue.html` save one of these as `.epilogue.html` inside a folder to customize it: * [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) ## example browser-js point `--js-browser` to one of these by URL: * [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders * [`quickmove.js`](quickmove.js) adds a hotkey to move selected files into a subfolder * [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading * [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API ## example any-js point `--js-browser` and/or `--js-other` to one of these by URL: * [`banner.js`](banner.js) shows a very enterprise [legal-banner](https://github.com/user-attachments/assets/8ae8e087-b209-449c-b08d-74e040f0284b) ## example browser-css point `--css-browser` to one of these by URL: * [`browser-icons.css`](browser-icons.css) adds filetype icons ## meadup.js * turns copyparty into chromecast just more flexible (and probably way more buggy) * usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js` # junk * [**rave.js**](./rave.js): april-fools joke, [demo (epilepsy warning)](https://cd.ocv.me/b/d2/d21/#af-9b927c42,sorthref), not maintained, very buggy ================================================ FILE: contrib/plugins/banner.js ================================================ (function() { // usage: copy this to '.banner.js' in your webroot, // and run copyparty with the following arguments: // --js-browser /.banner.js --js-other /.banner.js // had to pick the most chuuni one as the default var bannertext = '' + '

You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only.

' + '

By using this IS (which includes any device attached to this IS), you consent to the following conditions:

' + '
    ' + '
  • The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations.
  • ' + '
  • At any time, the USG may inspect and seize data stored on this IS.
  • ' + '
  • Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose.
  • ' + '
  • This IS includes security measures (e.g., authentication and access controls) to protect USG interests -- not for your personal benefit or privacy.
  • ' + '
  • Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.
  • ' + '
'; // fancy div to insert into pages function bannerdiv(border) { var ret = mknod('div', null, bannertext); if (border) ret.setAttribute("style", "border:1em solid var(--fg); border-width:.3em 0; margin:3em 0"); return ret; } // keep all of these false and then selectively enable them in the if-blocks below var show_msgbox = false, login_top = false, top = false, bottom = false, top_bordered = false, bottom_bordered = false; if (QS("h1#cc") && QS("a#k")) { // this is the controlpanel // (you probably want to keep just one of these enabled) show_msgbox = true; login_top = true; bottom = true; } else if (ebi("swin") && ebi("smac")) { // this is the connect-page, same deal here show_msgbox = true; top_bordered = true; bottom_bordered = true; } else if (ebi("op_cfg") || ebi("div#mw") ) { // we're running in the main filebrowser (op_cfg) or markdown-viewer/editor (div#mw), // fragile pages which break if you do something too fancy show_msgbox = true; } // shows a fullscreen messagebox; works on all pages if (show_msgbox) { var now = Math.floor(Date.now() / 1000), last_shown = sread("bannerts") || 0; // 60 * 60 * 17 = 17 hour cooldown if (now - last_shown > 60 * 60 * 17) { swrite("bannerts", now); modal.confirm(bannertext, null, function () { location = 'https://this-page-intentionally-left-blank.org/'; }); } } // show a message on the page footer; only works on the connect-page if (top || top_bordered) { var dst = ebi('wrap'); dst.insertBefore(bannerdiv(top_bordered), dst.firstChild); } // show a message on the page footer; only works on the controlpanel and connect-page if (bottom || bottom_bordered) { ebi('wrap').appendChild(bannerdiv(bottom_bordered)); } // show a message on the top of the page; only works on the controlpanel if (login_top) { var dst = QS('h1'); dst.parentNode.insertBefore(bannerdiv(false), dst); } })(); ================================================ FILE: contrib/plugins/browser-icons.css ================================================ /* video, alternative 1: top-left icon, just like the other formats ======================================================================= #ggrid>a:is( [href$=".mkv"i], [href$=".mp4"i], [href$=".webm"i], ):before { content: '📺'; } */ /* video, alternative 2: play-icon in the middle of the thumbnail ======================================================================= */ #ggrid>a:is( [href$=".mkv"i], [href$=".mp4"i], [href$=".webm"i], ) { position: relative; overflow: hidden; } #ggrid>a:is( [href$=".mkv"i], [href$=".mp4"i], [href$=".webm"i], ):before { content: '▶'; opacity: .8; margin: 0; padding: 1em .5em 1em .7em; border-radius: 9em; line-height: 0; color: #fff; text-shadow: none; background: rgba(0, 0, 0, 0.7); left: calc(50% - 1em); top: calc(50% - 1.4em); } /* audio */ #ggrid>a:is( [href$=".mp3"i], [href$=".ogg"i], [href$=".opus"i], [href$=".flac"i], [href$=".m4a"i], [href$=".aac"i], ):before { content: '🎵'; } /* image */ #ggrid>a:is( [href$=".jpg"i], [href$=".jpeg"i], [href$=".png"i], [href$=".gif"i], [href$=".webp"i], ):before { content: '🎨'; } ================================================ FILE: contrib/plugins/graft-thumbs.js ================================================ // USAGE: // place this file somewhere in the webroot and then // python3 -m copyparty --js-browser /.res/graft-thumbs.js // // DESCRIPTION: // this is a gridview plugin which, for each file in a folder, // looks for another file with the same filename (but with a // different file extension) // // if one of those files is an image and the other is not, // then this plugin assumes the image is a "sidecar thumbnail" // for the other file, and it will graft the image thumbnail // onto the non-image file (for example an mp3) // // optional feature 1, default-enabled: // the image-file is then hidden from the directory listing // // optional feature 2, default-enabled: // when clicking the audio file, the image will also open (function() { // `graft_thumbs` assumes the gridview has just been rendered; // it looks for sidecars, and transplants those thumbnails onto // the other file with the same basename (filename sans extension) var graft_thumbs = function () { if (!thegrid.en) return; // not in grid mode var files = msel.getall(), pairs = {}; console.log(files); for (var a = 0; a < files.length; a++) { var file = files[a], is_pic = /\.(jpe?g|png|gif|webp|jxl)$/i.exec(file.vp), is_audio = re_au_all.exec(file.vp), basename = file.vp.replace(/\.[^\.]+$/, ""), entry = pairs[basename]; if (!entry) // first time seeing this basename; create a new entry in pairs entry = pairs[basename] = {}; if (is_pic) entry.thumb = file; else if (is_audio) entry.audio = file; } var basenames = Object.keys(pairs); for (var a = 0; a < basenames.length; a++) (function(a) { var pair = pairs[basenames[a]]; if (!pair.thumb || !pair.audio) return; // not a matching pair of files var img_thumb = QS('#ggrid a[ref="' + pair.thumb.id + '"] img[onload]'), img_audio = QS('#ggrid a[ref="' + pair.audio.id + '"] img[onload]'); if (!img_thumb || !img_audio) return; // something's wrong... let's bail // alright, graft the thumb... img_audio.src = img_thumb.src; // ...and hide the sidecar img_thumb.closest('a').style.display = 'none'; // ...and add another onclick-handler to the audio, // so it also opens the pic while playing the song img_audio.addEventListener('click', function() { img_thumb.click(); return false; // let it bubble to the next listener }); })(a); }; // ...and then the trick! near the end of loadgrid, // thegrid.bagit is called to initialize the baguettebox // (image/video gallery); this is the perfect function to // "hook" (hijack) so we can run our code :^) // need to grab a backup of the original function first, var orig_func = thegrid.bagit; // and then replace it with our own: thegrid.bagit = function (isrc) { if (isrc !== '#ggrid') // we only want to modify the grid, so // let the original function handle this one return orig_func(isrc); graft_thumbs(); // when changing directories, the grid is // rendered before msel returns the correct // filenames, so schedule another run: setTimeout(graft_thumbs, 1); // and finally, call the original thegrid.bagit function return orig_func(isrc); }; if (ls0) { // the server included an initial listing json (ls0), // so the grid has already been rendered without our hook graft_thumbs(); } })(); ================================================ FILE: contrib/plugins/meadup.js ================================================ // USAGE: // place this file somewhere in the webroot and then // python3 -m copyparty --js-browser /memes/meadup.js // // FEATURES: // * adds an onscreen keyboard for operating a media center remotely, // relies on https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/very-bad-idea.py // * adds an interactive anime girl (if you can find the dependencies) var hambagas = [ "https://www.youtube.com/watch?v=pFA3KGp4GuU" ]; // keybaord, // onscreen keyboard by @steinuil function initKeybaord(BASE_URL, HAMBAGA, consoleLog, consoleError) { document.querySelector('.keybaord-container').innerHTML = `
esc
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
ins
del
\`
1
2
3
4
5
6
7
8
9
0
-
=
backspace
tab
q
w
e
r
t
y
u
i
o
p
[
]
enter
🍔
a
s
d
f
g
h
j
k
l
;
'
\\
shift
\\
z
x
c
v
b
n
m
,
.
/
shift
ctrl
win
alt
space
altgr
menu
ctrl
🔉
🔊
⬅️
⬇️
⬆️
➡️
PgUp
PgDn
🏠
End
`; function arraySample(array) { return array[Math.floor(Math.random() * array.length)]; } function sendMessage(msg) { return fetch(BASE_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, body: "msg=" + encodeURIComponent(msg), }).then( (r) => r.text(), // so the response body shows up in network tab (err) => consoleError(err) ); } const MODIFIER_ON_CLASS = "keybaord-modifier-on"; const KEY_DATASET = "data-keybaord-key"; const KEY_CLASS = "keybaord-key"; const modifiers = new Set() function toggleModifier(button, key) { button.classList.toggle(MODIFIER_ON_CLASS); if (modifiers.has(key)) { modifiers.delete(key); } else { modifiers.add(key); } } function popModifiers() { let modifierString = ""; modifiers.forEach((mod) => { document.querySelector("[" + KEY_DATASET + "='" + mod + "']") .classList.remove(MODIFIER_ON_CLASS); modifierString += mod + "+"; }); modifiers.clear(); return modifierString; } Array.from(document.querySelectorAll("." + KEY_CLASS)).forEach((button) => { const key = button.dataset.keybaordKey; button.addEventListener("click", (ev) => { switch (key) { case "HAMBAGA": sendMessage(arraySample(HAMBAGA)); break; case "Shift_L": case "Shift_R": case "Control_L": case "Control_R": case "Meta_L": case "Alt_L": case "Alt_R": toggleModifier(button, key); break; default: { const keyWithModifiers = popModifiers() + key; consoleLog(keyWithModifiers); sendMessage("key " + keyWithModifiers) .then(() => consoleLog(keyWithModifiers + " OK")); } } }); }); } // keybaord integration (function () { var o = mknod('div'); clmod(o, 'keybaord-container', 1); ebi('op_msg').appendChild(o); o = mknod('style'); o.innerHTML = ` .keybaord-body { display: flex; flex-flow: column nowrap; margin: .6em 0; } .keybaord-row { display: flex; } .keybaord-key { border: 1px solid rgba(128,128,128,0.2); width: 41px; height: 40px; display: flex; justify-content: center; align-items: center; } .keybaord-key:active { background-color: lightgrey; } .keybaord-key.keybaord-modifier-on { background-color: lightblue; } .keybaord-key.keybaord-backspace { width: 82px; } .keybaord-key.keybaord-tab { width: 55px; } .keybaord-key.keybaord-enter { width: 69px; } .keybaord-key.keybaord-capslock { width: 80px; } .keybaord-key.keybaord-backslash { width: 88px; } .keybaord-key.keybaord-lshift { width: 65px; } .keybaord-key.keybaord-rshift { width: 103px; } .keybaord-key.keybaord-lctrl { width: 55px; } .keybaord-key.keybaord-super { width: 55px; } .keybaord-key.keybaord-alt { width: 55px; } .keybaord-key.keybaord-altgr { width: 55px; } .keybaord-key.keybaord-what { width: 55px; } .keybaord-key.keybaord-rctrl { width: 55px; } .keybaord-key.keybaord-spacebar { width: 302px; } `; document.head.appendChild(o); initKeybaord('/', hambagas, (msg) => { toast.inf(2, msg.toString()) }, (msg) => { toast.err(30, msg.toString()) }); })(); // live2d (dumb pointless meme) // dependencies for this part are not tracked in git // so delete this section if you wanna use this file // (or supply your own l2d model and js) (function () { var o = mknod('link'); o.setAttribute('rel', 'stylesheet'); o.setAttribute('href', "/bad-memes/pio.css"); document.head.appendChild(o); o = mknod('style'); o.innerHTML = '.pio-container{text-shadow:none;z-index:1}'; document.head.appendChild(o); o = mknod('div'); clmod(o, 'pio-container', 1); o.innerHTML = '
'; document.body.appendChild(o); var remaining = 3; for (var a of ['pio', 'l2d', 'fireworks']) { import_js(`/bad-memes/${a}.js`, function () { if (remaining --> 1) return; o = mknod('script'); o.innerHTML = 'var pio = new Paul_Pio({"selector":[],"mode":"fixed","hidden":false,"content":{"close":"ok bye"},"model":["/bad-memes/sagiri/model.json"]});'; document.body.appendChild(o); }); } })(); ================================================ FILE: contrib/plugins/minimal-up2k.html ================================================ show advanced options ================================================ FILE: contrib/plugins/minimal-up2k.js ================================================ /* makes the up2k ui REALLY minimal by hiding a bunch of stuff almost the same as minimal-up2k.html except this one...: -- applies to every write-only folder when used with --js-browser -- only applies if javascript is enabled -- doesn't hide the total upload ETA display -- looks slightly better ======================== == USAGE INSTRUCTIONS == 1. create a volume which anyone can read from (if you haven't already) 2. copy this file into that volume, so anyone can download it 3. enable the plugin by telling the webbrowser to load this file; assuming the URL to the public volume is /res/, and assuming you're using config-files, then add this to your config: [global] js-browser: /res/minimal-up2k.js alternatively, if you're not using config-files, then add the following commandline argument instead: --js-browser=/res/minimal-up2k.js */ var u2min = ` show advanced options `; if (!has(perms, 'read')) { var e2 = mknod('div'); e2.innerHTML = u2min; ebi('wrap').insertBefore(e2, QS('#wfp')); } ================================================ FILE: contrib/plugins/quickmove.js ================================================ "use strict"; // USAGE: // place this file somewhere in the webroot, // for example in a folder named ".res" to hide it, and then // python3 copyparty-sfx.py -v .::A --js-browser /.res/quickmove.js // // DESCRIPTION: // the command above launches copyparty with one single volume; // ".::A" = current folder as webroot, and everyone has Admin // // the plugin adds hotkey "W" which moves all selected files // into a subfolder named "foobar" inside the current folder (function() { var action_to_perform = ask_for_confirmation_and_then_move; // this decides what the new hotkey should do; // ask_for_confirmation_and_then_move = show a yes/no box, // move_selected_files = just move the files immediately var move_destination = "foobar"; // this is the target folder to move files to; // by default it is a subfolder of the current folder, // but it can also be an absolute path like "/foo/bar" // === // === END OF CONFIG // === var main_hotkey_handler, // copyparty's original hotkey handler plugin_enabler, // timer to engage this plugin when safe files_to_move; // list of files to move function ask_for_confirmation_and_then_move() { var num_files = msel.getsel().length, msg = "move the selected " + num_files + " files?"; if (!num_files) return toast.warn(2, 'no files were selected to be moved'); modal.confirm(msg, move_selected_files, null); } function move_selected_files() { var selection = msel.getsel(); if (!selection.length) return toast.warn(2, 'no files were selected to be moved'); if (thegrid.bbox) { // close image/video viewer thegrid.bbox = null; baguetteBox.destroy(); } files_to_move = []; for (var a = 0; a < selection.length; a++) files_to_move.push(selection[a].vp); move_next_file(); } function move_next_file() { var num_files = files_to_move.length, filepath = files_to_move.pop(), filename = vsplit(filepath)[1]; toast.inf(10, "moving " + num_files + " files...\n\n" + filename); var dst = move_destination; if (!dst.endsWith('/')) // must have a trailing slash, so add it dst += '/'; if (!dst.startsWith('/')) // destination is a relative path, so prefix current folder path dst = get_evpath() + dst; // and finally append the filename dst += '/' + filename; // prepare the move-request to be sent var xhr = new XHR(); xhr.onload = xhr.onerror = function() { if (this.status !== 201) return toast.err(30, 'move failed: ' + esc(this.responseText)); if (files_to_move.length) return move_next_file(); // still more files to go toast.ok(1, 'move OK'); treectl.goto(); // reload the folder contents }; xhr.open('POST', filepath + '?move=' + dst); xhr.send(); } function our_hotkey_handler(e) { // bail if either ALT, CTRL, or SHIFT is pressed if (anymod(e)) return main_hotkey_handler(e); // let copyparty handle this keystroke var keycode = (e.key || e.code) + '', ae = document.activeElement, aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; // check the current aet (active element type), // only continue if one of the following currently has input focus: // nothing | link | button | table-row | table-cell | div | text if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet)) return main_hotkey_handler(e); // let copyparty handle this keystroke if (keycode == 'w' || keycode == 'KeyW') { // okay, this one's for us... do the thing action_to_perform(); return ev(e); } return main_hotkey_handler(e); // let copyparty handle this keystroke } function enable_plugin() { if (!window.hotkeys_attached) return console.log('quickmove is waiting for the page to finish loading'); clearInterval(plugin_enabler); main_hotkey_handler = document.onkeydown; document.onkeydown = our_hotkey_handler; console.log('quickmove is now enabled'); } // copyparty doesn't enable its hotkeys until the page // has finished loading, so we'll wait for that too plugin_enabler = setInterval(enable_plugin, 100); })(); ================================================ FILE: contrib/plugins/rave.js ================================================ /* untz untz untz untz */ (function () { var can, ctx, W, H, fft, buf, bars, barw, pv, hue = 0, ibeat = 0, beats = [9001], beats_url = '', uofs = 0, ops = ebi('ops'), raving = false, recalc = 0, cdown = 0, FC = 0.9, css = ``; QS('body').appendChild(mknod('div', null, css)); function rave_load() { console.log('rave_load'); can = mknod('canvas', 'fft'); QS('body').appendChild(can); ctx = can.getContext('2d'); fft = new AnalyserNode(actx, { "fftSize": 2048, "maxDecibels": 0, "smoothingTimeConstant": 0.7, }); ibeat = 0; beats = [9001]; buf = new Uint8Array(fft.frequencyBinCount); bars = buf.length * FC; afilt.filters.push(fft); if (!raving) { raving = true; raver(); } beats_url = mp.au.src.split('?')[0].replace(/(.*\/)(.*)/, '$1.beats/$2.txt'); console.log("reading beats from", beats_url); var xhr = new XHR(); xhr.open('GET', beats_url, true); xhr.onload = readbeats; xhr.url = beats_url; xhr.send(); } function rave_unload() { qsr('#fft'); can = null; } function readbeats() { if (this.url != beats_url) return console.log('old beats??', this.url, beats_url); var sbeats = this.responseText.replace(/\r/g, '').split(/\n/g); if (sbeats.length < 3) return; beats = []; for (var a = 0; a < sbeats.length; a++) beats.push(parseFloat(sbeats[a])); var end = beats.slice(-2), t = end[1], d = t - end[0]; while (d > 0.1 && t < 1200) beats.push(t += d); } function hrand() { return Math.random() - 0.5; } function raver() { if (!can) { raving = false; return; } requestAnimationFrame(raver); if (!mp || !mp.au || mp.au.paused) return; if (--uofs >= 0) { document.body.style.marginLeft = hrand() * uofs + 'px'; ebi('tree').style.marginLeft = hrand() * uofs + 'px'; for (var a of QSA('#ops>a, #path>a, #pctl>a')) a.style.transform = 'translate(' + hrand() * uofs * 1 + 'px, ' + hrand() * uofs * 0.7 + 'px) rotate(' + Math.random() * uofs * 0.7 + 'deg)' } if (--recalc < 0) { recalc = 60; var tree = ebi('tree'), x = tree.style.display == 'none' ? 0 : tree.offsetWidth; //W = can.width = window.innerWidth - x; //H = can.height = window.innerHeight; //H = ebi('widget').offsetTop; W = can.width = bars; H = can.height = 512; barw = 1; //parseInt(0.8 + W / bars); can.style.left = x + 'px'; can.style.width = (window.innerWidth - x) + 'px'; can.style.height = ebi('widget').offsetTop + 'px'; } //if (--cdown == 1) // clmod(ops, 'untz'); fft.getByteFrequencyData(buf); var imax = 0, vmax = 0; for (var a = 10; a < 50; a++) if (vmax < buf[a]) { vmax = buf[a]; imax = a; } hue = hue * 0.93 + imax * 0.07; ctx.fillStyle = 'rgba(0,0,0,0)'; ctx.fillRect(0, 0, W, H); ctx.clearRect(0, 0, W, H); ctx.fillStyle = 'hsla(' + (hue * 2.5) + ',100%,50%,0.7)'; var x = 0, mul = (H / 256) * 0.5; for (var a = 0; a < buf.length * FC; a++) { var v = buf[a] * mul * (1 + 0.69 * a / buf.length); ctx.fillRect(x, H - v, barw, v); x += barw; } var t = mp.au.currentTime + 0.05; if (ibeat >= beats.length || beats[ibeat] > t) return; while (ibeat < beats.length && beats[ibeat++] < t) continue; return untz(); var cv = 0; for (var a = 0; a < 128; a++) cv += buf[a]; if (cv - pv > 1000) { console.log(pv, cv, cv - pv); if (cdown < 0) { clmod(ops, 'untz', 1); cdown = 20; } } pv = cv; } function untz() { console.log('untz'); uofs = 14; document.body.animate([ { boxShadow: 'inset 0 0 1em #f0c' }, { boxShadow: 'inset 0 0 20em #f0c', offset: 0.2 }, { boxShadow: 'inset 0 0 0 #f0c' }, ], { duration: 200, iterations: 1 }); } afilt.plugs.push({ "en": true, "load": rave_load, "unload": rave_unload }); })(); ================================================ FILE: contrib/plugins/up2k-hook-ytid.js ================================================ // way more specific example -- // assumes all files dropped into the uploader have a youtube-id somewhere in the filename, // locates the youtube-ids and passes them to an API which returns a list of IDs which should be uploaded // // also tries to find the youtube-id in the embedded metadata // // assumes copyparty is behind nginx as /ytq is a standalone service which must be rproxied in place function up2k_namefilter(good_files, nil_files, bad_files, hooks) { var passthru = up2k.uc.fsearch; if (passthru) return hooks[0](good_files, nil_files, bad_files, hooks.slice(1)); a_up2k_namefilter(good_files, nil_files, bad_files, hooks).then(() => { }); } // ebi('op_up2k').appendChild(mknod('input','unick')); function bstrpos(buf, ptn) { var ofs = 0, ch0 = ptn[0], sz = buf.byteLength; while (true) { ofs = buf.indexOf(ch0, ofs); if (ofs < 0 || ofs >= sz) return -1; for (var a = 1; a < ptn.length; a++) if (buf[ofs + a] !== ptn[a]) break; if (a === ptn.length) return ofs; ++ofs; } } async function a_up2k_namefilter(good_files, nil_files, bad_files, hooks) { var t0 = Date.now(), yt_ids = new Set(), textdec = new TextDecoder('latin1'), md_ptn = new TextEncoder().encode('youtube.com/watch?v='), file_ids = [], // all IDs found for each good_files md_only = [], // `${id} ${fn}` where ID was only found in metadata mofs = 0, mnchk = 0, mfile = '', myid = localStorage.getItem('ytid_t0'); if (!myid) localStorage.setItem('ytid_t0', myid = Date.now()); for (var a = 0; a < good_files.length; a++) { var [fobj, name] = good_files[a], cname = name, // will clobber sz = fobj.size, ids = [], fn_ids = [], md_ids = [], id_ok = false, m; // all IDs found in this file file_ids.push(ids); // look for ID in filename; reduce the // metadata-scan intensity if the id looks safe m = /[\[(-]([\w-]{11})[\])]?\.(?:mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name); id_ok = !!m; while (true) { // fuzzy catch-all; // some ytdl fork did %(title)-%(id).%(ext) ... m = /(?:^|[^\w])([\w-]{11})(?:$|[^\w-])/.exec(cname); if (!m) break; cname = cname.replace(m[1], ''); yt_ids.add(m[1]); fn_ids.unshift(m[1]); } // look for IDs in video metadata, if (/\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name)) { toast.show('inf r', 0, `analyzing file ${a + 1} / ${good_files.length} :\n${name}\n\nhave analysed ${++mnchk} files in ${(Date.now() - t0) / 1000} seconds, ${humantime((good_files.length - (a + 1)) * (((Date.now() - t0) / 1000) / mnchk))} remaining,\n\nbiggest offset so far is ${mofs}, in this file:\n\n${mfile}`); // check first and last 128 MiB; // pWxOroN5WCo.mkv @ 6edb98 (6.92M) // Nf-nN1wF5Xo.mp4 @ 4a98034 (74.6M) var chunksz = 1024 * 1024 * 2, // byte aspan = id_ok ? 128 : 512; // MiB aspan = parseInt(Math.min(sz / 2, aspan * 1024 * 1024) / chunksz) * chunksz; if (!aspan) aspan = Math.min(sz, chunksz); for (var side = 0; side < 2; side++) { var ofs = side ? Math.max(0, sz - aspan) : 0, nchunks = aspan / chunksz; for (var chunk = 0; chunk < nchunks; chunk++) { var bchunk = await fobj.slice(ofs, ofs + chunksz + 16).arrayBuffer(), uchunk = new Uint8Array(bchunk, 0, bchunk.byteLength), bofs = bstrpos(uchunk, md_ptn), absofs = Math.min(ofs + bofs, (sz - ofs) + bofs), txt = bofs < 0 ? '' : textdec.decode(uchunk.subarray(bofs)), m; //console.log(`side ${ side }, chunk ${ chunk }, ofs ${ ofs }, bchunk ${ bchunk.byteLength }, txt ${ txt.length }`); while (true) { // mkv/webm have [a-z] immediately after url m = /(youtube\.com\/watch\?v=[\w-]{11})/.exec(txt); if (!m) break; txt = txt.replace(m[1], ''); m = m[1].slice(-11); console.log(`found ${m} @${bofs}, ${name} `); yt_ids.add(m); if (!has(fn_ids, m) && !has(md_ids, m)) { md_ids.push(m); md_only.push(`${m} ${name}`); } else // id appears several times; make it preferred md_ids.unshift(m); // bail after next iteration chunk = nchunks - 1; side = 9; if (mofs < absofs) { mofs = absofs; mfile = name; } } ofs += chunksz; if (ofs >= sz) break; } } } for (var yi of md_ids) ids.push(yi); for (var yi of fn_ids) if (!has(ids, yi)) ids.push(yi); } if (md_only.length) console.log('recovered the following youtube-IDs by inspecting metadata:\n\n' + md_only.join('\n')); else if (yt_ids.size) console.log('did not discover any additional youtube-IDs by inspecting metadata; all the IDs also existed in the filenames'); else console.log('failed to find any youtube-IDs at all, sorry'); if (false) { var msg = `finished analysing ${mnchk} files in ${(Date.now() - t0) / 1000} seconds,\n\nbiggest offset was ${mofs} in this file:\n\n${mfile}`, mfun = function () { toast.ok(0, msg); }; mfun(); setTimeout(mfun, 200); return hooks[0]([], [], [], hooks.slice(1)); } var el = ebi('unick'), unick = el ? el.value : ''; if (unick) { console.log(`sending uploader nickname [${unick}]`); fetch(document.location, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: 'msg=' + encodeURIComponent(unick) }); } toast.inf(5, `running query for ${yt_ids.size} youtube-IDs...`); var xhr = new XHR(); xhr.open('POST', '/ytq', true); xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.onload = xhr.onerror = function () { if (this.status != 200) return toast.err(0, `sorry, database query failed ;_;\n\nplease let us know so we can look at it, thx!!\n\nerror ${this.status}: ${(this.response && this.response.err) || this.responseText}`); process_id_list(this.responseText); }; xhr.send(Array.from(yt_ids).join('\n')); function process_id_list(txt) { var wanted_ids = new Set(txt.trim().split('\n')), name_id = {}, wanted_names = new Set(), // basenames with a wanted ID -- not including relpath wanted_names_scoped = {}, // basenames with a wanted ID -> list of dirs to search under wanted_files = new Set(); // filedrops for (var a = 0; a < good_files.length; a++) { var name = good_files[a][1]; for (var b = 0; b < file_ids[a].length; b++) if (wanted_ids.has(file_ids[a][b])) { // let the next stage handle this to prevent dupes //wanted_files.add(good_files[a]); var m = /(.*)\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name); if (!m) continue; var [rd, fn] = vsplit(m[1]); if (fn in wanted_names_scoped) wanted_names_scoped[fn].push(rd); else wanted_names_scoped[fn] = [rd]; wanted_names.add(fn); name_id[m[1]] = file_ids[a][b]; break; } } // add all files with the same basename as each explicitly wanted file // (infojson/chatlog/etc when ID was discovered from metadata) for (var a = 0; a < good_files.length; a++) { var [rd, name] = vsplit(good_files[a][1]); for (var b = 0; b < 3; b++) { name = name.replace(/\.[^\.]+$/, ''); if (!wanted_names.has(name)) continue; var vid_fp = false; for (var c of wanted_names_scoped[name]) if (rd.startsWith(c)) vid_fp = c + name; if (!vid_fp) continue; var subdir = name_id[vid_fp]; subdir = `v${subdir.slice(0, 1)}/${subdir}-${myid}`; var newpath = subdir + '/' + good_files[a][1].split(/\//g).pop(); // check if this file is a dupe for (var c of good_files) if (c[1] == newpath) newpath = null; if (!newpath) break; good_files[a][1] = newpath; wanted_files.add(good_files[a]); break; } } function upload_filtered() { if (!wanted_files.size) return modal.alert('Good news -- turns out we already have all those.\n\nBut thank you for checking in!'); hooks[0](Array.from(wanted_files), nil_files, bad_files, hooks.slice(1)); } function upload_all() { hooks[0](good_files, nil_files, bad_files, hooks.slice(1)); } var n_skip = good_files.length - wanted_files.size, msg = `you added ${good_files.length} files; ${good_files.length == n_skip ? 'all' : n_skip} of them were skipped --\neither because we already have them,\nor because there is no youtube-ID in your filenames.\n\nOK / Enter = continue uploading just the ${wanted_files.size} files we definitely need\n\nCancel / ESC = override the filter; upload ALL the files you added`; if (!n_skip) upload_filtered(); else modal.confirm(msg, upload_filtered, upload_all); }; } up2k_hooks.push(function () { up2k.gotallfiles.unshift(up2k_namefilter); }); // persist/restore nickname field if present setInterval(function () { var o = ebi('unick'); if (!o || document.activeElement == o) return; o.oninput = function () { localStorage.setItem('unick', o.value); }; o.value = localStorage.getItem('unick') || ''; }, 1000); ================================================ FILE: contrib/plugins/up2k-hooks.js ================================================ // hooks into up2k function up2k_namefilter(good_files, nil_files, bad_files, hooks) { // is called when stuff is dropped into the browser, // after iterating through the directory tree and discovering all files, // before the upload confirmation dialogue is shown // good_files will successfully upload // nil_files are empty files and will show an alert in the final hook // bad_files are unreadable and cannot be uploaded var file_lists = [good_files, nil_files, bad_files]; // build a list of filenames var filenames = []; for (var lst of file_lists) for (var ent of lst) filenames.push(ent[1]); toast.inf(5, "running database query..."); // simulate delay while passing the list to some api for checking setTimeout(function () { // only keep webm files as an example var new_lists = []; for (var lst of file_lists) { var keep = []; new_lists.push(keep); for (var ent of lst) if (/\.webm$/.test(ent[1])) keep.push(ent); } // finally, call the next hook in the chain [good_files, nil_files, bad_files] = new_lists; hooks[0](good_files, nil_files, bad_files, hooks.slice(1)); }, 1000); } // register up2k_hooks.push(function () { up2k.gotallfiles.unshift(up2k_namefilter); }); ================================================ FILE: contrib/podman-systemd/README.md ================================================ # copyparty with Podman and Systemd Use this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service. Documentation for `.container` files can be found in the [Container unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container) docs. Systemd does not understand `.container` files natively, so Podman converts these to `.service` files with a [systemd-generator](https://www.freedesktop.org/software/systemd/man/latest/systemd.generator.html). This process is transparent, but sometimes needs to be debugged in case your `.container` file is malformed. There are instructions to debug the systemd generator in the Troubleshooting section below. To run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation There is a sample configuration file in the same directory as this file (`copyparty.conf`). ## Run the container as root Running the container as the root user is easy to set up, but less secure. There are instructions in the next section to run the container as a rootless user if you'd rather run the container like that. First, change this line in the `copyparty.container` file to reflect the directory you want to share. By default, it shares `/mnt/` but you'll probably want to change that. ``` # Change /mnt to something you want to share Volume=/mnt:/w:z ``` Note that you can select the owner and group of this volume by changing the `uid:` and `gid:` of the volume in `copyparty.conf`, but for simplicity let's assume you want it to be owned by `root:root`. To install and start copyparty with Podman and systemd as the root user, run the following: ```shell sudo mkdir -pv /etc/containers/systemd/ /etc/copyparty/ sudo cp -v copyparty.container /etc/containers/systemd/ sudo cp -v copyparty.conf /etc/copyparty/ sudo systemctl daemon-reload sudo systemctl start copyparty ``` Note: You can't "enable" this kind of Podman service. The `[Install]` section of the `.container` file effectively handles enabling the service so that it starts when the server reboots. You can see the status of the service with: ```shell sudo systemctl status -a copyparty ``` You can see (and follow) the logs with either of these commands: ```shell sudo podman logs -f copyparty # -a is required or else you'll get output like: copyparty[549025]: [649B blob data] sudo journalctl -a -f -u copyparty ``` ## Run the container as a non-root user This configuration is more secure, but is more involved and requires ensuring files have proper permissions. You will need a root user account to do some of this setup. First, you need a user to run the container as. In this example we'll create a "podman" user with UID=1001 and GID=1001. ```shell sudo groupadd -g 1001 podman sudo useradd -u 1001 -m podman sudo usermod -aG podman podman sudo loginctl enable-linger podman # Set a strong password for this user sudo -u podman passwd ``` The `enable-linger` command allows the podman user to run systemd user services that persist even when the user is not logged in. You could use a user that already exists in the system to run this service as, just make sure to run `loginctl enable-linger USERNAME` for that user. Next, change these lines in the `copyparty.container` file to reflect the config directory and the directory you want to share. By default, the config shares `/home/podman/copyparty/sharing/` but you'll probably want to change this: ``` # Change to reflect your non-root user's home directory Volume=/home/podman/copyparty/config:/cfg:z # Change to the directory you want to share Volume=/home/podman/copyparty/sharing:/w:z ``` Make sure the podman user has read/write access to both of these directories. Next, **log in to the server as the podman user**. To install and start copyparty as the non-root podman user, run the following: ```shell mkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config cp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container cp -v copyparty.conf /home/podman/copyparty/config systemctl --user daemon-reload systemctl --user start copyparty ``` **Important note: Never use `sudo` with `systemctl --user`!** You can check the status of the user service with: ```shell systemctl --user status -a copyparty ``` You can see (and follow) the logs with: ```shell podman logs -f copyparty journalctl --user -a -f -u copyparty ``` ## Troubleshooting If the container fails to start, and you've modified the `.container` service, it's likely that your `.container` file failed to be translated into a `.service` file. You can debug the podman service generator with this command: ```shell sudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun ``` ## Allowing Traffic from Outside your Server To allow traffic on port 3923 of your server, you should run: ```shell sudo firewall-cmd --permanent --add-port=3923/tcp sudo firewall-cmd --reload ``` Otherwise, you won't be able to access the copyparty server from anywhere other than the server itself. ## Updating copyparty To update the version of copyparty used in the container, you can: ```shell # If root: sudo podman pull docker.io/copyparty/ac:latest sudo systemctl restart copyparty # If non-root: podman pull docker.io/copyparty/ac:latest systemctl --user restart copyparty ``` Or, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run: ```shell # If root: sudo systemctl daemon-reload sudo systemctl restart copyparty # If non-root: systemctl --user daemon-reload systemctl --user restart copyparty ``` Podman will pull the image you've specified when restarting. If you have it set to `:latest`, Podman does not know to re-pull the container. ### Enabling auto-update Alternatively, you can enable auto-updates by un-commenting this line: ``` # AutoUpdate=registry ``` You will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with: ```shell # If root: sudo systemctl enable podman-auto-update.timer podman-auto-update.service # If non-root: systemctl --user enable podman-auto-update.timer podman-auto-update.service ``` This works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours. ================================================ FILE: contrib/podman-systemd/copyparty.conf ================================================ [global] e2dsa # enable file indexing and filesystem scanning e2ts # and enable multimedia indexing ansi # and colors in log messages # uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl; # $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd) # and copyparty replaces %Y-%m%d with Year-MonthDay, so the # full path will be something like /var/log/copyparty/2023-1130.txt # (note: enable compression by adding .xz at the end) # q, lo: $LOGS_DIRECTORY/%Y-%m%d.log # p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE) # i: 127.0.0.1 # only allow connections from localhost (reverse-proxies) # ftp: 3921 # enable ftp server on port 3921 # p: 3939 # listen on another port # df: 16 # stop accepting uploads if less than 16 GB free disk space # ver # show copyparty version in the controlpanel # grid # show thumbnails/grid-view by default # theme: 2 # monokai # name: datasaver # change the server-name that's displayed in the browser # stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow) # no-robots, force-js # make it harder for search engines to read your server # enable version-checking by uncommenting one of the vc-url lines below; # shows a warning-banner in the controlpanel if your version has a known vulnerability #vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9 #vc-url: https://api.copyparty.eu/advisories vc-exit # panic and shutdown instead of just showing the warning [accounts] ed: wark # username: password [/] # create a volume at "/" (the webroot), which will /w # share the contents of the "/w" folder accs: rw: * # everyone gets read-write access, but rwmda: ed # the user "ed" gets read-write-move-delete-admin # uid: 1000 # If you're running as root, you can change the owner of this volume here # gid: 1000 # If you're running as root, you can change the group of this volume here ================================================ FILE: contrib/podman-systemd/copyparty.container ================================================ [Container] # It's recommended to replace :latest with a specific version # for example: docker.io/copyparty/ac:1.19.15 Image=docker.io/copyparty/ac:latest ContainerName=copyparty # Uncomment to enable auto-updates # AutoUpdate=registry # Environment variables # enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram) Environment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE # ensures log-messages are not delayed (but can reduce speed a tiny bit) Environment=PYTHONUNBUFFERED=1 # Ports PublishPort=3923:3923 # Volumes (PLEASE LOOK!) # Rootful setup: # Leave as-is # Non-root setup: # Change /etc/copyparty to /home//copyparty/config Volume=/etc/copyparty:/cfg:z # Rootful setup: # Change /mnt to the directory you want to share # Non-root setup: # Change /mnt to something owned by your user, e.g., /home//copyparty/sharing:/w:z Volume=/mnt:/w:z # Give the container time to stop in case the thumbnailer is still running. # It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer StopTimeout=15 # hide it from logs with "/._" so it matches the default --lf-url filter HealthCmd="wget --spider -q 127.0.0.1:3923/?reset=/._" HealthInterval=1m HealthTimeout=2s HealthRetries=5 HealthStartPeriod=15s [Unit] After=default.target [Install] # Start by default on boot WantedBy=default.target [Service] # Give the container time to start in case it needs to pull the image TimeoutStartSec=600 ================================================ FILE: contrib/rc/copyparty ================================================ #!/bin/sh # # PROVIDE: copyparty # REQUIRE: networking # KEYWORD: . /etc/rc.subr name="copyparty" rcvar="copyparty_enable" copyparty_user="copyparty" copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit copyparty_command="/usr/local/bin/python3.11 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}" pidfile="/var/run/copyparty/${name}.pid" command="/usr/sbin/daemon" command_args="-P ${pidfile} -r -f ${copyparty_command}" stop_postcmd="copyparty_shutdown" copyparty_shutdown() { if [ -e "${pidfile}" ]; then echo "Stopping supervising daemon." kill -s TERM `cat ${pidfile}` fi } load_rc_config $name : ${copyparty_enable:=no} run_rc_command "$1" ================================================ FILE: contrib/send-to-cpp.contextlet.json ================================================ { "code": "// https://addons.mozilla.org/en-US/firefox/addon/contextlets/\n// https://github.com/davidmhammond/contextlets\n\nvar url = 'http://partybox.local:3923/';\nvar pw = 'wark';\n\nvar xhr = new XMLHttpRequest();\nxhr.msg = this.info.linkUrl || this.info.srcUrl;\nxhr.open('POST', url, true);\nxhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');\nxhr.setRequestHeader('PW', pw);\nxhr.send('msg=' + xhr.msg);\n", "contexts": [ "link" ], "icons": null, "patterns": "", "scope": "background", "title": "send to cpp", "type": "normal" } ================================================ FILE: contrib/setup-ashell.sh ================================================ #!/bin/bash # # this script will install copyparty onto an iOS device (iPhone/iPad) # # step 1: install a-Shell: # https://apps.apple.com/us/app/a-shell/id1473805438 # # step 2: copypaste the following command into a-Shell: # curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh # # step 3: launch copyparty with this command: cpp # # if you ever want to upgrade copyparty, just repeat step 2 cd "$HOME/Documents" curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py # create the config file? (cannot use heredoc because body too large) [ -e cpc ] || { echo '[global]' >cpc echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc echo ' e2ts # and enable multimedia indexing' >>cpc echo ' ver # show copyparty version in the controlpanel' >>cpc echo ' qrz: 2 # enable qr-code and make it big' >>cpc echo ' qrp: 1 # reduce qr-code padding' >>cpc echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc echo '' >>cpc echo ' # enable these by uncommenting them:' >>cpc echo ' # ftp: 21 # enable ftp server on port 21' >>cpc echo ' # tftp: 69 # enable tftp server on port 69' >>cpc echo '' >>cpc echo '[/]' >>cpc echo ' ~/Documents' >>cpc echo ' accs:' >>cpc echo ' A: *' >>cpc } # create the launcher? [ -e cpp ] || { echo '#!/bin/sh' >cpp echo '' >>cpp echo '# change the font so the qr-code draws correctly:' >>cpp echo 'config -n "Menlo" # name' >>cpp echo 'config -s 8 # size' >>cpp echo '' >>cpp echo '# launch copyparty' >>cpp echo 'exec copyparty -c cpc "$@"' >>cpp } chmod 755 copyparty cpp echo echo ================================= echo echo 'okay, all done!' echo echo 'you can edit your config' echo 'with this command: vim cpc' echo echo 'you can run copyparty' echo 'with this command: cpp' echo ================================================ FILE: contrib/sharex.sxcu ================================================ { "Version": "15.0.0", "Name": "copyparty", "DestinationType": "ImageUploader", "RequestMethod": "POST", "RequestURL": "http://127.0.0.1:3923/sharex", "Parameters": { "j": null }, "Headers": { "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE" }, "Body": "MultipartFormData", "Arguments": { "act": "bput" }, "FileFormName": "f", "URL": "{json:files[0].url}" } ================================================ FILE: contrib/sharex12.sxcu ================================================ { "Name": "copyparty", "DestinationType": "ImageUploader, TextUploader, FileUploader", "RequestURL": "http://127.0.0.1:3923/sharex", "FileFormName": "f", "Arguments": { "act": "bput" }, "Headers": { "accept": "url", "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE" } } ================================================ FILE: contrib/systemd/cfssl.service ================================================ # NOTE: this is now a built-in feature in copyparty # but you may still want this if you have specific needs # # systemd service which generates a new TLS certificate on each boot, # that way the one-year expiry time won't cause any issues -- # just have everyone trust the ca.pem once every 10 years # # assumptions/placeholder values: # * this script and copyparty runs as user "cpp" # * copyparty repo is at ~cpp/dev/copyparty # * CA is named partylan # * server IPs = 10.1.2.3 and 192.168.123.1 # * server hostname = party.lan [Unit] Description=copyparty certificate generator Before=copyparty.service [Service] User=cpp Type=oneshot SyslogIdentifier=cpp-cert ExecStart=/bin/bash -c 'cd ~/dev/copyparty/contrib && ./cfssl.sh partylan 10.1.2.3,192.168.123.1,party.lan y' [Install] WantedBy=multi-user.target ================================================ FILE: contrib/systemd/copyparty-user.service ================================================ # this will start `/usr/bin/copyparty` # and read config from `$HOME/.config/copyparty.conf` # # unless you add -q to disable logging, you may want to remove the # following line to allow buffering (slightly better performance): # Environment=PYTHONUNBUFFERED=x [Unit] Description=copyparty file server [Service] Type=notify SyslogIdentifier=copyparty WorkingDirectory=/var/lib/copyparty-jail Environment=PYTHONUNBUFFERED=x Environment=PRTY_CONFIG=%h/.config/copyparty/copyparty.conf ExecReload=/bin/kill -s USR1 $MAINPID # ensure there is a config ExecStartPre=/bin/bash -c 'if [[ ! -f %h/.config/copyparty/copyparty.conf ]]; then mkdir -p %h/.config/copyparty; cp /etc/copyparty/copyparty.conf %h/.config/copyparty/copyparty.conf; fi' # run copyparty ExecStart=/usr/bin/python3 /usr/bin/copyparty [Install] WantedBy=default.target ================================================ FILE: contrib/systemd/copyparty.conf ================================================ [global] i: 127.0.0.1 [accounts] user: password [/] /var/lib/copyparty-jail accs: r: * rwdma: user flags: grid ================================================ FILE: contrib/systemd/copyparty.example.conf ================================================ # not actually YAML but lets pretend: # -*- mode: yaml -*- # vim: ft=yaml: # put this file in /etc/ [global] e2dsa # enable file indexing and filesystem scanning e2ts # and enable multimedia indexing ansi # and colors in log messages # disable logging to stdout/journalctl and log to a file instead; # $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd) # and copyparty replaces %Y-%m%d with Year-MonthDay, so the # full path will be something like /var/log/copyparty/2023-1130.txt # (note: enable compression by adding .xz at the end) q, lo: $LOGS_DIRECTORY/%Y-%m%d.log # enable version-checker by uncommenting one of the 'vc-url' lines below; this will # periodically check if your copyparty version has a known security vulnerability, # showing a warning on /?h (control-panel) for all users with permission 'a' or 'A' #vc-url: https://api.github.com/repos/9001/copyparty/security-advisories?per_page=9 #vc-url: https://api.copyparty.eu/advisories vc-age: 3 # how many hours to wait between each version-check vc-exit # emergency-exit if a version-check indicates that the current version is vulnerable # p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE) # i: 127.0.0.1 # only allow connections from localhost (reverse-proxies) # ftp: 3921 # enable ftp server on port 3921 # p: 3939 # listen on another port # df: 16 # stop accepting uploads if less than 16 GB free disk space # ver # show copyparty version in the controlpanel # grid # show thumbnails/grid-view by default # theme: 2 # monokai # name: datasaver # change the server-name that's displayed in the browser # stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow) # no-robots, force-js # make it harder for search engines to read your server [accounts] ed: wark # username: password [/] # create a volume at "/" (the webroot), which will /mnt # share the contents of the "/mnt" folder accs: rw: * # everyone gets read-write access, but rwmda: ed # the user "ed" gets read-write-move-delete-admin ================================================ FILE: contrib/systemd/copyparty.service ================================================ # this will start `/usr/local/bin/copyparty-sfx.py` and # read copyparty config from `/etc/copyparty.conf`, for example: # https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.conf # # installation: # wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py # useradd -r -s /sbin/nologin -m -d /var/lib/copyparty copyparty # firewall-cmd --permanent --add-port=3923/tcp # --zone=libvirt # firewall-cmd --reload # cp -pv copyparty.service /etc/systemd/system/ # cp -pv copyparty.conf /etc/ # restorecon -vr /etc/systemd/system/copyparty.service # on fedora/rhel # systemctl daemon-reload && systemctl enable --now copyparty # # every time you edit this file, you must "systemctl daemon-reload" # for the changes to take effect and then "systemctl restart copyparty" # # if it fails to start, first check this: systemctl status copyparty # then try starting it while viewing logs: # journalctl -fan 100 # tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log) # # if you run into any issues, for example thumbnails not working, # try removing the "some quick hardening" section and then please # let me know if that actually helped so we can look into it # # you may want to: # - change "User=copyparty" and "/var/lib/copyparty/" to another user # - edit /etc/copyparty.conf to configure copyparty # and in the ExecStart= line: # - change '/usr/bin/python3' to another interpreter # # with `Type=notify`, copyparty will signal systemd when it is ready to # accept connections; correctly delaying units depending on copyparty. # But note that journalctl will get the timestamps wrong due to # python disabling line-buffering, so messages are out-of-order: # https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png # ######################################################################## ######################################################################## [Unit] Description=copyparty file server [Service] Type=notify SyslogIdentifier=copyparty Environment=PYTHONUNBUFFERED=x ExecReload=/bin/kill -s USR1 $MAINPID PermissionsStartOnly=true ## user to run as + where the TLS certificate is (if any) ## User=copyparty Group=copyparty WorkingDirectory=/var/lib/copyparty Environment=XDG_CONFIG_HOME=/var/lib/copyparty/.config ## OPTIONAL: allow copyparty to listen on low ports (like 80/443); ## you need to uncomment the "p: 80,443,3923" in the config too ## ------------------------------------------------------------ ## a slightly safer alternative is to enable partyalone.service ## which does portforwarding with nftables instead, but an even ## better option is to use a reverse-proxy (nginx/caddy/...) ## AmbientCapabilities=CAP_NET_BIND_SERVICE ## some quick hardening; TODO port more from the nixos package ## MemoryMax=50% MemorySwapMax=50% ProtectClock=true ProtectControlGroups=true ProtectHostname=true ProtectKernelLogs=true ProtectKernelModules=true ProtectKernelTunables=true ProtectProc=invisible RemoveIPC=true RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true ## create a directory for logfiles; ## this defines $LOGS_DIRECTORY which is used in copyparty.conf ## LogsDirectory=copyparty ## finally, start copyparty and give it the config file: ## ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -c /etc/copyparty.conf # NOTE: if you installed copyparty from an OS package repo (nice) # then you probably want something like this instead: #ExecStart=/usr/bin/copyparty -c /etc/copyparty.conf [Install] WantedBy=multi-user.target ================================================ FILE: contrib/systemd/copyparty@.service ================================================ # this will start `/usr/bin/copyparty` # and read config from `/etc/copyparty/copyparty.conf` # # the %i refers to whatever you put after the copyparty@ # so with copyparty@foo.service, %i == foo # # unless you add -q to disable logging, you may want to remove the # following line to allow buffering (slightly better performance): # Environment=PYTHONUNBUFFERED=x [Unit] Description=copyparty file server [Service] Type=notify SyslogIdentifier=copyparty WorkingDirectory=/var/lib/copyparty-jail Environment=PYTHONUNBUFFERED=x Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf ExecReload=/bin/kill -s USR1 $MAINPID # user to run as + where the TLS certificate is (if any) User=%i Environment=XDG_CONFIG_HOME=/home/%i/.config # run copyparty ExecStart=/usr/bin/python3 /usr/bin/copyparty [Install] WantedBy=multi-user.target ================================================ FILE: contrib/systemd/index.md ================================================ this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured please edit `/etc/copyparty/copyparty.conf` (if running as a system service) or `$HOME/.config/copyparty/copyparty.conf` if running as a user service a basic configuration example is available at https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.example.conf a configuration example that explains most flags is available at https://github.com/9001/copyparty/blob/hovudstraum/docs/chungus.conf the full list of configuration options can be seen at https://ocv.me/copyparty/helptext.html or by running `copyparty --help` ================================================ FILE: contrib/systemd/prisonparty.service ================================================ # this will start `/usr/local/bin/copyparty-sfx.py` # in a chroot, preventing accidental access elsewhere, # and share '/mnt' with anonymous read+write # # installation: # 1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin # 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty # # expose additional filesystem locations to copyparty # by listing them between the last `cpp` and `--` # # `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000) # # you may want to: # change '/mnt::rw' to another location or permission-set # (remember to change the '/mnt' chroot arg too) # # unless you add -q to disable logging, you may want to remove the # following line to allow buffering (slightly better performance): # Environment=PYTHONUNBUFFERED=x [Unit] Description=copyparty file server [Service] SyslogIdentifier=prisonparty Environment=PYTHONUNBUFFERED=x WorkingDirectory=/var/lib/copyparty-jail ExecReload=/bin/kill -s USR1 $MAINPID # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' # run copyparty ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail cpp cpp \ /mnt \ -- \ /usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw [Install] WantedBy=multi-user.target ================================================ FILE: contrib/systemd/prisonparty@.service ================================================ # this will start `/usr/bin/copyparty` # in a chroot, preventing accidental access elsewhere, # and read copyparty config from `/etc/copyparty/copyparty.conf` # # expose additional filesystem locations to copyparty # by listing them between the last `%i` and `--` # # `%i %i` = user/group to run copyparty as; can be IDs (1000 1000) # the %i refers to whatever you put after the prisonparty@ # so with prisonparty@foo.service, %i == foo # # unless you add -q to disable logging, you may want to remove the # following line to allow buffering (slightly better performance): # Environment=PYTHONUNBUFFERED=x [Unit] Description=copyparty file server [Service] Type=notify SyslogIdentifier=prisonparty WorkingDirectory=/var/lib/copyparty-jail Environment=PYTHONUNBUFFERED=x Environment=PRTY_CONFIG=/etc/copyparty/copyparty.conf ExecReload=/bin/kill -s USR1 $MAINPID # user to run as + where the TLS certificate is (if any) User=%i Environment=XDG_CONFIG_HOME=/home/%i/.config # run copyparty ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail %i %i \ /etc/copyparty \ -- \ /usr/bin/python3 /usr/bin/copyparty [Install] WantedBy=multi-user.target ================================================ FILE: contrib/themes/bsod.css ================================================ /* copy bsod.* into a folder named ".themes" in your webroot and then --themes=10 --theme=9 --css-browser=/.themes/bsod.css */ html.ey { --w2: #3d7bbc; --w3: #5fcbec; --fg: #fff; --fg-max: #fff; --fg-weak: var(--w3); --bg: #2067b2; --bg-d3: var(--bg); --bg-d2: var(--w2); --bg-d1: var(--fg-weak); --bg-u2: var(--bg); --bg-u3: var(--bg); --bg-u5: var(--w2); --tab-alt: var(--fg-weak); --row-alt: var(--w2); --scroll: var(--w3); --a: #fff; --a-b: #fff; --a-hil: #fff; --a-h-bg: var(--fg-weak); --a-dark: var(--a); --a-gray: var(--fg-weak); --btn-fg: var(--a); --btn-bg: var(--w2); --btn-h-fg: var(--w2); --btn-1-fg: var(--bg); --btn-1-bg: var(--a); --txt-sh: a; --txt-bg: var(--w2); --u2-b1-bg: var(--w2); --u2-b2-bg: var(--w2); --u2-txt-bg: var(--w2); --u2-tab-bg: a; --u2-tab-1-bg: var(--w2); --sort-1: var(--a); --sort-1: var(--fg-weak); --tree-bg: var(--bg); --g-b1: a; --g-b2: a; --g-f-bg: var(--w2); --f-sh1: 0.1; --f-sh2: 0.02; --f-sh3: 0.1; --f-h-b1: a; --srv-1: var(--a); --srv-3: var(--a); --mp-sh: a; } html.ey { background: url('bsod.png') top 5em right 4.5em no-repeat fixed var(--bg); } html.ey body#b { background: var(--bg); /*sandbox*/ } html.ey #ops { margin: 1.7em 1.5em 0 1.5em; border-radius: .3em; border-width: 1px 0; } html.ey #ops a { text-shadow: 1px 1px 0 rgba(0,0,0,0.5); } html.ey .opbox { margin: 1.5em 0 0 0; } html.ey #tree { box-shadow: none; } html.ey #tt { border-color: var(--w2); background: var(--w2); } html.ey .mdo a { background: none; text-decoration: underline; } html.ey .mdo pre, html.ey .mdo code { color: #fff; background: var(--w2); border: none; } html.ey .mdo h1, html.ey .mdo h2 { background: none; border-color: var(--w2); } html.ey .mdo ul ul, html.ey .mdo ul ol, html.ey .mdo ol ul, html.ey .mdo ol ol { border-color: var(--w2); } html.ey .mdo p>em, html.ey .mdo li>em, html.ey .mdo td>em { color: #fd0; } ================================================ FILE: contrib/traefik/copyparty.yaml ================================================ # ./traefik --configFile=copyparty.yaml entryPoints: web: address: :8080 transport: # don't disconnect during big uploads respondingTimeouts: readTimeout: "0s" log: level: DEBUG providers: file: # WARNING: must be same filename as current file filename: "copyparty.yaml" http: services: service-cpp: loadBalancer: servers: - url: "http://127.0.0.1:3923/" routers: my-router: rule: "PathPrefix(`/`)" service: service-cpp ================================================ FILE: contrib/webdav-cfg.bat ================================================ @echo off rem removes the 47.6 MiB filesize limit when downloading from webdav rem + optionally allows/enables password-auth over plaintext http rem + optionally helps disable wpad, removing the 10sec latency net session >nul 2>&1 if %errorlevel% neq 0 ( echo sorry, you must run this as administrator pause exit /b ) reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v FileSizeLimitInBytes /t REG_DWORD /d 0xffffffff /f reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters /v FsCtlRequestTimeoutInSec /t REG_DWORD /d 0xffffffff /f echo( echo OK; echo allow webdav basic-auth over plaintext http? echo Y: login works, but the password will be visible in wireshark etc echo N: login will NOT work unless you use https and valid certificates choice if %errorlevel% equ 1 ( reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v BasicAuthLevel /t REG_DWORD /d 0x2 /f rem default is 1 (require tls) ) echo( echo OK; echo do you want to disable wpad? echo can give a HUGE speed boost depending on network settings choice if %errorlevel% equ 1 ( echo( echo i'm about to open the [Connections] tab in [Internet Properties] for you; echo please click [LAN settings] and disable [Automatically detect settings] echo( pause control inetcpl.cpl,,4 ) net stop webclient net start webclient echo( echo OK; all done pause ================================================ FILE: contrib/windows/copyparty-ctmp.bat ================================================ rem run copyparty.exe on machines with busted environment variables cmd /v /c "set TMP=\tmp && copyparty.exe" ================================================ FILE: contrib/zfs-tune.py ================================================ #!/usr/bin/env python3 import os import sqlite3 import sys import traceback """ when the up2k-database is stored on a zfs volume, this may give slightly higher performance (actual gains not measured yet) NOTE: must be applied in combination with the related advice in the openzfs documentation; https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Workload%20Tuning.html#database-workloads and see specifically the SQLite subsection it is assumed that all databases are stored in a single location, for example with `--hist /var/store/hists` three alternatives for running this script: 1. copy it into /var/store/hists and run "python3 zfs-tune.py s" (s = modify all databases below folder containing script) 2. cd into /var/store/hists and run "python3 ~/zfs-tune.py w" (w = modify all databases below current working directory) 3. python3 ~/zfs-tune.py /var/store/hists if you use docker, run copyparty with `--hist /cfg/hists`, copy this script into /cfg, and run this: podman run --rm -it --entrypoint /usr/bin/python3 ghcr.io/9001/copyparty-ac /cfg/zfs-tune.py s """ PAGESIZE = 65536 # borrowed from copyparty; short efficient stacktrace for errors def min_ex(max_lines: int = 8, reverse: bool = False) -> str: et, ev, tb = sys.exc_info() stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1] fmt = "%s:%d <%s>: %s" ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb] if et or ev or tb: ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev)) return "\n".join(ex[-max_lines:][:: -1 if reverse else 1]) def set_pagesize(db_path): try: # check current page_size with sqlite3.connect(db_path) as db: v = db.execute("pragma page_size").fetchone()[0] if v == PAGESIZE: print(" `-- OK") return # https://www.sqlite.org/pragma.html#pragma_page_size # `- disable wal; set pagesize; vacuum # (copyparty will reenable wal if necessary) with sqlite3.connect(db_path) as db: db.execute("pragma journal_mode=delete") db.commit() with sqlite3.connect(db_path) as db: db.execute(f"pragma page_size = {PAGESIZE}") db.execute("vacuum") print(" `-- new pagesize OK") except Exception: err = min_ex().replace("\n", "\n -- ") print(f"FAILED: {db_path}\n -- {err}") def main(): top = os.path.dirname(os.path.abspath(__file__)) cwd = os.path.abspath(os.getcwd()) try: x = sys.argv[1] except: print(f""" this script takes one mandatory argument: specify 's' to start recursing from folder containing this script file ({top}) specify 'w' to start recursing from the current working directory ({cwd}) specify a path to start recursing from there """) sys.exit(1) if x.lower() == "w": top = cwd elif x.lower() != "s": top = x for dirpath, dirs, files in os.walk(top): for fname in files: if not fname.endswith(".db"): continue db_path = os.path.join(dirpath, fname) print(db_path) set_pagesize(db_path) if __name__ == "__main__": main() ================================================ FILE: copyparty/__init__.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os import platform import sys import time # fmt: off _:tuple[int,int]=(0,0) # _____________________________________________________________________ hey there! if you are reading this, your python is too old to run copyparty without some help. Please use https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py or the pypi package instead, or see https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building if you want to build it yourself :-) ************************************************************************************************************************************************ # fmt: on try: from typing import TYPE_CHECKING except: TYPE_CHECKING = False if True: from typing import Any, Callable, Optional PY2 = sys.version_info < (3,) PY36 = sys.version_info > (3, 6) if not PY2: unicode: Callable[[Any], str] = str else: sys.dont_write_bytecode = True unicode = unicode # type: ignore WINDOWS: Any = ( [int(x) for x in platform.version().split(".")] if platform.system() == "Windows" else False ) VT100 = "--ansi" in sys.argv or ( os.environ.get("NO_COLOR", "").lower() in ("", "0", "false") and sys.stdout.isatty() and "--no-ansi" not in sys.argv and (not WINDOWS or WINDOWS >= [10, 0, 14393]) ) # introduced in anniversary update ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"] MACOS = platform.system() == "Darwin" GRAAL = platform.python_implementation() == "GraalVM" EXE = bool(getattr(sys, "frozen", False)) try: CORES = len(os.sched_getaffinity(0)) except: CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2 # all embedded resources to be retrievable over http zs = """ web/a/partyfuse.py web/a/u2c.py web/a/webdav-cfg.txt web/baguettebox.js web/browser.css web/browser.html web/browser.js web/browser2.html web/cf.html web/copyparty.gif web/deps/busy.mp3 web/deps/easymde.css web/deps/easymde.js web/deps/marked.js web/deps/fuse.py web/deps/mini-fa.css web/deps/mini-fa.woff web/deps/prism.css web/deps/prism.js web/deps/prismd.css web/deps/scp.woff2 web/deps/sha512.ac.js web/deps/sha512.hw.js web/idp.html web/iiam.gif web/md.css web/md.html web/md.js web/md2.css web/md2.js web/mde.css web/mde.html web/mde.js web/msg.html web/opds.xml web/rups.css web/rups.html web/rups.js web/shares.css web/shares.html web/shares.js web/splash.css web/splash.html web/splash.js web/svcs.html web/svcs.js web/tl/chi.js web/tl/cze.js web/tl/deu.js web/tl/epo.js web/tl/fin.js web/tl/fra.js web/tl/grc.js web/tl/hun.js web/tl/ita.js web/tl/jpn.js web/tl/kor.js web/tl/nld.js web/tl/nno.js web/tl/nor.js web/tl/pol.js web/tl/por.js web/tl/rus.js web/tl/spa.js web/tl/swe.js web/tl/tur.js web/tl/ukr.js web/tl/vie.js web/ui.css web/up2k.js web/util.js web/w.hash.js """ RES = set(zs.strip().split("\n")) RESM = { "web/a/partyfuse.txt": "web/a/partyfuse.py", "web/a/u2c.txt": "web/a/u2c.py", "web/a/webdav-cfg.bat": "web/a/webdav-cfg.txt", } class EnvParams(object): def __init__(self) -> None: self.t0 = time.time() self.mod = "" self.mod_ = "" self.cfg = "" self.scfg = True E = EnvParams() ================================================ FILE: copyparty/__main__.py ================================================ #!/usr/bin/env python3 # coding: utf-8 from __future__ import print_function, unicode_literals """copyparty: http file sharing hub (py2/py3)""" __author__ = "ed " __copyright__ = 2019 __license__ = "MIT" __url__ = "https://github.com/9001/copyparty/" import argparse import base64 import locale import os import re import select import socket import sys import threading import time import traceback import uuid from .__init__ import ( ANYWIN, CORES, EXE, MACOS, PY2, PY36, VT100, WINDOWS, E, EnvParams, unicode, ) from .__version__ import CODENAME, S_BUILD_DT, S_VERSION from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt from .bos import bos from .cfg import flagcats, onedash from .svchub import SvcHub from .util import ( APPLESAN_TXT, BAD_BOTS, DEF_EXP, DEF_MTE, DEF_MTH, HAVE_IPV6, IMPLICATIONS, JINJA_VER, MIKO_VER, MIMES, PARTFTPY_VER, PY_DESC, PYFTPD_VER, RAM_AVAIL, RAM_TOTAL, RE_ANSI, SQLITE_VER, UNPLICATIONS, URL_BUG, URL_PRJ, Daemon, align_tab, b64enc, ctypes, dedent, has_resource, load_resource, min_ex, pybin, read_utf8, termsize, wk32, wrap, ) if True: # pylint: disable=using-constant-test from collections.abc import Callable from types import FrameType from typing import Any, Optional if PY2: range = xrange # type: ignore try: if os.environ.get("PRTY_NO_TLS"): raise Exception() HAVE_SSL = True import ssl except: HAVE_SSL = False u = unicode printed: list[str] = [] zsid = uuid.uuid4().urn[4:] CFG_DEF = [os.environ.get("PRTY_CONFIG", "")] if not CFG_DEF[0]: CFG_DEF.pop() class RiceFormatter(argparse.HelpFormatter): def __init__(self, *args: Any, **kwargs: Any) -> None: if PY2: kwargs["width"] = termsize()[0] super(RiceFormatter, self).__init__(*args, **kwargs) def _get_help_string(self, action: argparse.Action) -> str: """ same as ArgumentDefaultsHelpFormatter(HelpFormatter) except the help += [...] line now has colors """ fmt = "\033[36m (default: \033[35m%(default)s\033[36m)\033[0m" if not VT100: fmt = " (default: %(default)s)" ret = unicode(action.help) if "%(default)" not in ret: if action.default is not argparse.SUPPRESS: defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] if action.option_strings or action.nargs in defaulting_nargs: ret += fmt if not VT100: ret = re.sub("\033\\[[0-9;]+m", "", ret) return ret # type: ignore def _fill_text(self, text: str, width: int, indent: str) -> str: """same as RawDescriptionHelpFormatter(HelpFormatter)""" return "".join(indent + line + "\n" for line in text.splitlines()) def __add_whitespace(self, idx: int, iWSpace: int, text: str) -> str: return (" " * iWSpace) + text if idx else text def _split_lines(self, text: str, width: int) -> list[str]: # https://stackoverflow.com/a/35925919 textRows = text.splitlines() ptn = re.compile(r"\s*[0-9\-]{0,}\.?\s*") for idx, line in enumerate(textRows): search = ptn.search(line) if not line.strip(): textRows[idx] = " " elif search: lWSpace = search.end() lines = [ self.__add_whitespace(i, lWSpace, x) for i, x in enumerate(wrap(line, width, width - 1)) ] textRows[idx] = lines # type: ignore return [item for sublist in textRows for item in sublist] class Dodge11874(RiceFormatter): def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs["width"] = 9003 super(Dodge11874, self).__init__(*args, **kwargs) class BasicDodge11874( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter ): def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs["width"] = 9003 super(BasicDodge11874, self).__init__(*args, **kwargs) def lprint(*a: Any, **ka: Any) -> None: eol = ka.pop("end", "\n") txt: str = " ".join(unicode(x) for x in a) + eol printed.append(txt) if not VT100: txt = RE_ANSI.sub("", txt) print(txt, end="", **ka) def warn(msg: str) -> None: lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg)) def init_E(EE: EnvParams) -> None: # some cpython alternatives (such as pyoxidizer) can # __init__ several times, so do expensive stuff here E = EE # pylint: disable=redefined-outer-name def get_unixdir() -> tuple[str, bool]: paths: list[tuple[Callable[..., Any], str]] = [ (os.environ.get, "XDG_CONFIG_HOME"), (os.path.expanduser, "~/.config"), (os.environ.get, "TMPDIR"), (os.environ.get, "TEMP"), (os.environ.get, "TMP"), (unicode, "/tmp"), ] errs = [] for npath, (pf, pa) in enumerate(paths): priv = npath < 2 # private/trusted location ram = npath > 1 # "nonvolatile"; not semantically same as `not priv` p = "" try: p = pf(pa) if not p or p.startswith("~"): continue p = os.path.normpath(p) mkdir = not os.path.isdir(p) if mkdir: os.mkdir(p, 0o700) p = os.path.join(p, "copyparty") if not priv and os.path.isdir(p): uid = os.geteuid() if os.stat(p).st_uid != uid: p += ".%s" % (uid,) if os.path.isdir(p) and os.stat(p).st_uid != uid: raise Exception("filesystem has broken unix permissions") try: os.listdir(p) except: os.mkdir(p, 0o700) if ram: t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/" errs.append(t % (pa, p)) elif mkdir: t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)" errs.append(t % (pa, p, " instead" if npath else "", pa)) elif errs: errs.append("Using %s/copyparty [%s] instead" % (pa, p)) if errs: warn(". ".join(errs)) return p, priv except Exception as ex: if p: t = "Unable to store config in %s [%s] due to %r" errs.append(t % (pa, p, ex)) t = "could not find a writable path for runtime state:\n> %s" raise Exception(t % ("\n> ".join(errs))) E.mod = os.path.dirname(os.path.realpath(__file__)) if E.mod.endswith("__init__"): E.mod = os.path.dirname(E.mod) E.mod_ = os.path.join(E.mod, "") try: p = os.environ.get("XDG_CONFIG_HOME") if not p: raise Exception() if p.startswith("~"): p = os.path.expanduser(p) p = os.path.abspath(os.path.realpath(p)) p = os.path.join(p, "copyparty") if not os.path.isdir(p): os.mkdir(p, 0o700) os.listdir(p) except: p = "" if p: E.cfg = p elif sys.platform == "win32": bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "." E.cfg = os.path.normpath(bdir + "/copyparty") elif sys.platform == "darwin": E.cfg = os.path.expanduser("~/Library/Preferences/copyparty") else: E.cfg, E.scfg = get_unixdir() E.cfg = E.cfg.replace("\\", "/") try: bos.makedirs(E.cfg, bos.MKD_700) except: if not os.path.isdir(E.cfg): raise def get_srvname(verbose) -> str: try: ret: str = unicode(socket.gethostname()).split(".")[0] except: ret = "" if ret not in ["", "localhost"]: return ret fp = os.path.join(E.cfg, "name.txt") if verbose: lprint("using hostname from {}\n".format(fp)) try: return read_utf8(None, fp, True).strip() except: ret = "" namelen = 5 while len(ret) < namelen: ret += base64.b32encode(os.urandom(4))[:7].decode("utf-8").lower() ret = re.sub("[234567=]", "", ret)[:namelen] with open(fp, "wb") as f: f.write(ret.encode("utf-8") + b"\n") return ret def get_salt(name: str, nbytes: int) -> str: fp = os.path.join(E.cfg, "%s-salt.txt" % (name,)) try: return read_utf8(None, fp, True).strip() except: ret = b64enc(os.urandom(nbytes)) with open(fp, "wb") as f: f.write(ret + b"\n") return ret.decode("utf-8") def ensure_locale() -> None: if ANYWIN and PY2: return # maybe XP, so busted 65001 safe = "en_US.UTF-8" for x in [ safe, "English_United States.UTF8", "English_United States.1252", ]: try: locale.setlocale(locale.LC_ALL, x) if x != safe: lprint("Locale: {}\n".format(x)) return except: continue t = "setlocale {} failed,\n sorting and dates might get funky\n" warn(t.format(safe)) def ensure_webdeps() -> None: if has_resource(E, "web/deps/mini-fa.woff"): return t = """could not find webdeps; if you are running the sfx, or exe, or pypi package, or docker image, then this is a bug! Please let me know so I can fix it, thanks :-) %s however, if you are a dev, or running copyparty from source, and you want full client functionality, you will need to build or obtain the webdeps: %s/blob/hovudstraum/docs/devnotes.md#building """ warn(t % (URL_BUG, URL_PRJ)) def configure_ssl_ver(al: argparse.Namespace) -> None: def terse_sslver(txt: str) -> str: txt = txt.lower() for c in ["_", "v", "."]: txt = txt.replace(c, "") return txt.replace("tls10", "tls1") # oh man i love openssl # check this out # hold my beer assert ssl # type: ignore # !rm ptn = re.compile(r"^OP_NO_(TLS|SSL)v") sslver = terse_sslver(al.ssl_ver).split(",") flags = [k for k in ssl.__dict__ if ptn.match(k)] # SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3 if "help" in sslver: avail1 = [terse_sslver(x[6:]) for x in flags] avail = " ".join(sorted(avail1) + ["all"]) lprint("\navailable ssl/tls versions:\n " + avail) sys.exit(0) al.ssl_flags_en = 0 al.ssl_flags_de = 0 for flag in sorted(flags): ver = terse_sslver(flag[6:]) num = getattr(ssl, flag) if ver in sslver: al.ssl_flags_en |= num else: al.ssl_flags_de |= num if sslver == ["all"]: x = al.ssl_flags_en al.ssl_flags_en = al.ssl_flags_de al.ssl_flags_de = x for k in ["ssl_flags_en", "ssl_flags_de"]: num = getattr(al, k) lprint("{0}: {1:8x} ({1})".format(k, num)) # think i need that beer now def configure_ssl_ciphers(al: argparse.Namespace) -> None: assert ssl # type: ignore # !rm ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) if al.ssl_ver: ctx.options &= ~al.ssl_flags_en ctx.options |= al.ssl_flags_de is_help = al.ciphers == "help" if al.ciphers and not is_help: try: ctx.set_ciphers(al.ciphers) except: lprint("\n\033[1;31mfailed to set ciphers\033[0m\n") if not hasattr(ctx, "get_ciphers"): lprint("cannot read cipher list: openssl or python too old") else: ciphers = [x["description"] for x in ctx.get_ciphers()] lprint("\n ".join(["\nenabled ciphers:"] + align_tab(ciphers) + [""])) if is_help: sys.exit(0) def args_from_cfg(cfg_path: str) -> list[str]: lines: list[str] = [] expand_config_file(None, lines, cfg_path, "") lines = upgrade_cfg_fmt(None, argparse.Namespace(vc=False), lines, "") ret: list[str] = [] skip = True for ln in lines: sn = ln.split(" #")[0].strip() if sn.startswith("["): skip = True if sn.startswith("[global]"): skip = False continue if skip or not sn.split("#")[0].strip(): continue for k, v in split_cfg_ln(sn).items(): k = k.lstrip("-") if not k: continue prefix = "-" if k in onedash else "--" if v is True: ret.append(prefix + k) else: ret.append(prefix + k + "=" + v) return ret def expand_cfg(argv) -> list[str]: if CFG_DEF: supp = args_from_cfg(CFG_DEF[0]) argv = argv[:1] + supp + argv[1:] n = 0 while n < len(argv): v1 = argv[n] v1v = v1[2:].lstrip("=") try: v2 = argv[n + 1] except: v2 = "" n += 1 if v1 == "-c" and v2 and os.path.isfile(v2): n += 1 argv = argv[:n] + args_from_cfg(v2) + argv[n:] elif v1.startswith("-c") and v1v and os.path.isfile(v1v): argv = argv[:n] + args_from_cfg(v1v) + argv[n:] return argv def quotecheck(al): for zs1, zco in vars(al).items(): zsl = [u(x) for x in zco] if isinstance(zco, list) else [u(zco)] for zs2 in zsl: zs2 = zs2.strip() zs3 = zs2.strip("\"'") if zs2 == zs3 or len(zs2) - len(zs3) < 2: continue if al.c: t = "found the following global-config: %s: %s\n values should not be quoted; did you mean: %s: %s" else: t = 'found the following config-option: "--%s=%s"\n values should not be quoted; did you mean: "--%s=%s"' warn(t % (zs1, zs2, zs1, zs3)) def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) -> None: msg = [""] * 5 for th in threading.enumerate(): stk = sys._current_frames()[th.ident] # type: ignore msg.append(str(th)) msg.extend(traceback.format_stack(stk)) msg.append("\n") print("\n".join(msg)) def disable_quickedit() -> None: if not ctypes: raise Exception("no ctypes") import atexit from ctypes import wintypes def ecb(ok: bool, fun: Any, args: list[Any]) -> list[Any]: if not ok: err: int = ctypes.get_last_error() # type: ignore if err: raise ctypes.WinError(err) # type: ignore return args if PY2: wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) k32 = wk32 k32.GetStdHandle.errcheck = ecb # type: ignore k32.GetConsoleMode.errcheck = ecb # type: ignore k32.SetConsoleMode.errcheck = ecb # type: ignore k32.GetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.LPDWORD) k32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD) def cmode(out: bool, mode: Optional[int] = None) -> int: h = k32.GetStdHandle(-11 if out else -10) if mode: return k32.SetConsoleMode(h, mode) # type: ignore cmode = wintypes.DWORD() k32.GetConsoleMode(h, ctypes.byref(cmode)) return cmode.value # disable quickedit mode = orig_in = cmode(False) quickedit = 0x40 extended = 0x80 mask = quickedit + extended if mode & mask != extended: atexit.register(cmode, False, orig_in) cmode(False, mode & ~mask | extended) # enable colors in case the os.system("rem") trick ever stops working if VT100: mode = orig_out = cmode(True) if mode & 4 != 4: atexit.register(cmode, True, orig_out) cmode(True, mode | 4) def sfx_tpoke(top: str): if os.environ.get("PRTY_NO_TPOKE"): return files = [top] + [ os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df ] while True: t = int(time.time()) for f in list(files): try: os.utime(f, (t, t)) except Exception as ex: lprint(" [%s] %r" % (f, ex)) files.remove(f) time.sleep(78123) def showlic() -> None: try: with load_resource(E, "res/COPYING.txt") as f: buf = f.read() except: buf = b"" if buf: print(buf.decode("utf-8", "replace")) else: print("no relevant license info to display") return def get_sects(): return [ [ "bind", "configure listening", dedent( """ \033[33m-i\033[0m takes a comma-separated list of interfaces to listen on; IP-addresses, unix-sockets, and/or open file descriptors the default (\033[32m-i ::\033[0m) means all IPv4 and IPv6 addresses \033[32m-i 0.0.0.0\033[0m listens on all IPv4 NICs/subnets \033[32m-i 127.0.0.1\033[0m listens on IPv4 localhost only \033[32m-i 127.1\033[0m listens on IPv4 localhost only \033[32m-i 127.1,192.168.123.1\033[0m = IPv4 localhost and 192.168.123.1 \033[33m-p\033[0m takes a comma-separated list of tcp ports to listen on; the default is \033[32m-p 3923\033[0m but as root you can \033[32m-p 80,443,3923\033[0m when running behind a reverse-proxy, it's recommended to use unix-sockets for improved performance and security; \033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on \033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m group. This is the best approach. Alternatively, \033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone can access it; bad unless it's inside a restricted folder \033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission (usually \033[33m0600\033[0m) and the same user/group as copyparty \033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3 \033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs """ ), ], [ "accounts", "accounts and volumes", dedent( """ -a takes username:password, -v takes src:dst:\033[33mperm\033[0m1:\033[33mperm\033[0m2:\033[33mperm\033[0mN:\033[32mvolflag\033[0m1:\033[32mvolflag\033[0m2:\033[32mvolflag\033[0mN:... * "\033[33mperm\033[0m" is "permissions,username1,username2,..." * "\033[32mvolflag\033[0m" is config flags to set on this volume --grp takes groupname:username1,username2,... and groupnames can be used instead of usernames in -v by prefixing the groupname with @ list of permissions: "r" (read): list folder contents, download files "w" (write): upload files; need "r" to see the uploads "m" (move): move files and folders; need "w" at destination "d" (delete): permanently delete files and folders "g" (get): download files, but cannot see folder contents "G" (upget): "get", but can see filekeys of their own uploads "h" (html): "get", but folders return their index.html "." (dots): user can ask to show dotfiles in listings "a" (admin): can see uploader IPs, config-reload "A" ("all"): same as "rwmda." (read/write/move/delete/admin/dotfiles) too many volflags to list here, see --help-flags example:\033[35m -a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m mount current directory at "/" with * r (read-only) for everyone * rw (read+write) for ed mount ../inc at "/dump" with * w (write-only) for everyone * rw (read+write) for ed * reject duplicate files \033[0m if no accounts or volumes are configured, current folder will be read/write for everyone the group \033[33m@acct\033[0m will always have every user with an account (the name of that group can be changed with \033[32m--grp-all\033[0m) to hide a volume from authenticated users, specify \033[33m*,-@acct\033[0m to subtract \033[33m@acct\033[0m from \033[33m*\033[0m (can subtract users from groups too) consider the config file for more flexible account/volume management, including dynamic reload at runtime (and being more readable w) see \033[32m--help-auth\033[0m for ways to provide the password in requests; see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares """ ), ], [ "auth", "how to login from a client", dedent( """ different ways to provide the password so you become authenticated: login with the ui: go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there send the password in the '\033[36mPW\033[0m' http-header: \033[36mPW: \033[35mhunter2\033[0m or if you have \033[33m--usernames\033[0m enabled, \033[36mPW: \033[35med:hunter2\033[0m send the password in the URL itself: \033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m or if you have \033[33m--usernames\033[0m enabled, \033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m use basic-authentication: \033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m which should be the same as this header: \033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m """ ), ], [ "auth-ord", "authentication precedence", dedent( """ \033[33m--auth-ord\033[0m is a comma-separated list of auth options (one or more of the [\033[35moptions\033[0m] below); first one wins [\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header, or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie (see \033[33m--help-auth\033[0m) [\033[35midp\033[0m] is a username provided in the http-request-header defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is provided by an authentication middleware such as authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m) [\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header, [\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header; [\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m] [\033[35mipu\033[0m] is a mapping from an IP-address to a username, auto-authing that client-IP to that account (see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m) NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list, it may still be enabled and can still take effect if none of the other alternatives identify the user NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button will be hidden when any idp feature is enabled """ ), ], [ "flags", "list of volflags", dedent( """ volflags are appended to volume definitions, for example, to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags: \033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub\033[0m if global config defines a volflag for all volumes, you can unset it for a specific volume with -flag """ ).rstrip() + build_flags_desc(), ], [ "handlers", "use plugins to handle certain events", dedent( """ usually copyparty returns a \033[33m404\033[0m if a file does not exist, and \033[33m403\033[0m if a user tries to access a file they don't have access to you can load a plugin which will be invoked right before this happens, and the plugin can choose to override this behavior load the plugin using --args or volflags; for example \033[36m --on404 ~/partyhandlers/not404.py -v .::r:c,on404=~/partyhandlers/not404.py \033[0m the file must define the function \033[35mmain(cli,vn,rem)\033[0m: \033[35mcli\033[0m: the copyparty HttpCli instance \033[35mvn\033[0m: the VFS which overlaps with the requested URL \033[35mrem\033[0m: the remainder of the URL below the VFS mountpoint `main` must return a string; one of the following: > \033[32m"true"\033[0m: the plugin has responded to the request, and the TCP connection should be kept open > \033[32m"false"\033[0m: the plugin has responded to the request, and the TCP connection should be terminated > \033[32m"retry"\033[0m: the plugin has done something to resolve the 404 situation, and copyparty should reattempt reading the file. if it still fails, a regular 404 will be returned > \033[32m"allow"\033[0m: should ignore the insufficient permissions and let the client continue anyways > \033[32m""\033[0m: the plugin has not handled the request; try the next plugin or return the usual 404 or 403 \033[1;35mPS!\033[0m the folder that contains the python file should ideally not contain many other python files, and especially nothing with filenames that overlap with modules used by copyparty """ ), ], [ "hooks", "execute commands before/after various events", dedent( """ execute a command (a program or script) before or after various events; \033[36mxbu\033[35m executes CMD before a file upload starts \033[36mxau\033[35m executes CMD after a file upload finishes \033[36mxiu\033[35m executes CMD after all uploads finish and volume is idle \033[36mxbc\033[35m executes CMD before a file copy \033[36mxac\033[35m executes CMD after a file copy \033[36mxbr\033[35m executes CMD before a file rename/move \033[36mxar\033[35m executes CMD after a file rename/move \033[36mxbd\033[35m executes CMD before a file delete \033[36mxad\033[35m executes CMD after a file delete \033[36mxm\033[35m executes CMD on message \033[36mxban\033[35m executes CMD if someone gets banned \033[0m can be defined as --args or volflags; for example \033[36m --xau foo.py -v .::r:c,xau=bar.py \033[0m hooks specified as commandline --args are appended to volflags; each commandline --arg and volflag can be specified multiple times, each hook will execute in order unless one returns non-zero, or "100" which means "stop daisychaining and return 0 (success/OK)" optionally prefix the command with comma-sep. flags similar to -mtp: \033[36mf\033[35m forks the process, doesn't wait for completion \033[36mc\033[35m checks return code, blocks the action if non-zero \033[36mj\033[35m provides json with info as 1st arg instead of filepath \033[36ms\033[35m provides input data on stdin (instead of 1st arg) \033[36mwN\033[35m waits N sec after command has been started before continuing \033[36mtN\033[35m sets an N sec timeout before the command is abandoned \033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5) \033[36mI\033[35m import and run as module, not as subprocess \033[36mar\033[35m only run hook if user has read-access \033[36marw\033[35m only run hook if user has read-write-access \033[36marwmd\033[35m ...and so on... (doesn't work for xiu or xban) \033[36mkt\033[35m kills the entire process tree on timeout (default), \033[36mkm\033[35m kills just the main process \033[36mkn\033[35m lets it continue running until copyparty is terminated \033[36mc0\033[35m show all process output (default) \033[36mc1\033[35m show only stderr \033[36mc2\033[35m show only stdout \033[36mc3\033[35m mute all process output \033[0m examples: \033[36m--xm some.py\033[35m runs \033[33msome.py msgtxt\033[35m on each 📟 message; \033[33mmsgtxt\033[35m is the message that was written into the web-ui \033[36m--xm j,some.py\033[35m runs \033[33msome.py jsontext\033[35m on each 📟 message; \033[33mjsontext\033[35m is the message info (ip, user, ..., msg-text) \033[36m--xm aw,j,some.py\033[35m requires user to have write-access \033[36m--xm aw,,notify-send,hey,--\033[35m shows an OS alert on linux; the \033[33m,,\033[35m stops copyparty from reading the rest as flags and the \033[33m--\033[35m stops notify-send from reading the message as args and the alert will be "hey" followed by the messagetext \033[36m--xm s,,tee,-a,log.txt\033[35m appends each msg to log.txt; \033[36m--xm s,j,,tee,-a,log.txt\033[35m writes it as json instead \033[36m--xau zmq:pub:tcp://*:5556\033[35m announces uploads on zeromq; \033[36m--xau t3,zmq:push:tcp://*:5557\033[35m also works, and you can \033[36m--xau t3,j,zmq:req:tcp://localhost:5555\033[35m too for example \033[0m each hook is executed once for each event, except for \033[36mxiu\033[0m which builds up a backlog of uploads, running the hook just once as soon as the volume has been idle for iN seconds (5 by default) \033[36mxiu\033[0m is also unique in that it will pass the metadata to the executed program on STDIN instead of as argv arguments (so just like the \033[36ms\033[0m option does for the other hook types), and it also includes the wark (file-id/hash) as a json property \033[36mxban\033[0m can be used to overrule / cancel a user ban event; if the program returns 0 (true/OK) then the ban will NOT happen effects can be used to redirect uploads into other locations, and to delete or index other files based on new uploads, but with certain limitations. See bin/hooks/reloc* and docs/devnotes.md#hook-effects the \033[36mI\033[0m option will override most other options, because it entirely hands over control to the hook, which is then able to tamper with copyparty's internal memory and wreck havoc if it wants to -- but this is worh it because it makes the hook 140x faster except for \033[36mxm\033[0m, only one hook / one action can run at a time, so it's recommended to use the \033[36mf\033[0m flag unless you really need to wait for the hook to finish before continuing (without \033[36mf\033[0m the upload speed can easily drop to 10% for small files)""" ), ], [ "idp", "replacing the login system with fancy middleware", dedent( """ if you already have a centralized service which handles user-authentication for other services already, you can integrate copyparty with that for automatic login if the middleware is providing the username in an http-header named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m if the middleware is providing a list of groups in the header named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m if the middleware is providing a header named '\033[35mAccount\033[0m' and the value is '\033[35malice@forest.net\033[0m' but the username is actually '\033[35mmarisa\033[0m' then do this for each user: \033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m (the separator '\033[35m^\033[0m' can be any character) make ABSOLUTELY SURE that the header can only be set by your middleware and not by clients! and, as an extra precaution, send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword) and then \033[36m--idp-h-key finalmasterspark\033[0m to require that the login/logout links/buttons can be replaced with links going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m will expand \033[36m{dst}\033[0m to the URL of the current page, so the IdP can redirect the user back to where they were """ ), ], [ "urlform", "how to handle url-form POSTs", dedent( """ values for --urlform: \033[36mstash\033[35m dumps the data to file and returns length + checksum \033[36msave,get\033[35m dumps to file and returns the page like a GET \033[36mprint \033[35m prints the data to log and returns an error \033[36mprint,xm \033[35m prints the data to log and returns --xm output \033[36mprint,get\033[35m prints the data to log and returns GET\033[0m note that the \033[35m--xm\033[0m hook will only run if \033[35m--urlform\033[0m is either \033[36mprint\033[0m or \033[36mprint,get\033[0m or the default \033[36mprint,xm\033[0m if an \033[35m--xm\033[0m hook returns text, then the response code will be HTTP 202; http/get responses will be HTTP 200 if there are multiple \033[35m--xm\033[0m hooks defined, then the first hook that produced output is returned if there are no \033[35m--xm\033[0m hooks defined, then the default \033[36mprint,xm\033[0m behaves like \033[36mprint,get\033[0m (returning html) """ ), ], [ "exp", "text expansion", dedent( """ specify --exp or the "exp" volflag to enable placeholder expansions in README.md / PREADME.md / .prologue.html / .epilogue.html --exp-md (volflag exp_md) holds the list of placeholders which can be expanded in READMEs, and --exp-lg (volflag exp_lg) likewise for logues; any placeholder not given in those lists will be ignored and shown as-is the default list will expand the following placeholders: \033[36m{{self.ip}} \033[35mclient ip \033[36m{{self.ua}} \033[35mclient user-agent \033[36m{{self.uname}} \033[35mclient username \033[36m{{self.host}} \033[35mthe "Host" header, or the server's external IP otherwise \033[36m{{cfg.name}} \033[35mthe --name global-config \033[36m{{cfg.logout}} \033[35mthe --logout global-config \033[36m{{vf.scan}} \033[35mthe "scan" volflag \033[36m{{vf.thsize}} \033[35mthumbnail size \033[36m{{srv.itime}} \033[35mserver time in seconds \033[36m{{srv.htime}} \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC) \033[36m{{hdr.cf-ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank) \033[0m so the following types of placeholders can be added to the lists: * any client header can be accessed through {{hdr.*}} * any variable in httpcli.py can be accessed through {{self.*}} * any global server setting can be accessed through {{cfg.*}} * any volflag can be accessed through {{vf.*}} remove vf.scan from default list using --exp-md /vf.scan add "accept" header to def. list using --exp-md +hdr.accept for performance reasons, expansion only happens while embedding documents into directory listings, and when accessing a ?doc=... link, but never otherwise, so if you click a -txt- link you'll have to refresh the page to apply expansion """ ), ], [ "ls", "volume inspection", dedent( """ \033[35m--ls USR,VOL,FLAGS \033[36mUSR\033[0m is a user to browse as; * is anonymous, ** is all users \033[36mVOL\033[0m is a single volume to scan, default is * (all vols) \033[36mFLAG\033[0m is flags; \033[36mv\033[0m in addition to realpaths, print usernames and vpaths \033[36mln\033[0m only prints symlinks leaving the volume mountpoint \033[36mp\033[0m exits 1 if any such symlinks are found \033[36mr\033[0m resumes startup after the listing examples: --ls '**' # list all files which are possible to read --ls '**,*,ln' # check for dangerous symlinks --ls '**,*,ln,p,r' # check, then start normally if safe """ ), ], [ "dbd", "database durability profiles", dedent( """ mainly affects uploads of many small files on slow HDDs; speeds measured uploading 520 files on a WD20SPZX (SMR 2.5" 5400rpm 4kb) \033[32macid\033[0m = extremely safe but slow; the old default. Should never lose any data no matter what \033[32mswal\033[0m = 2.4x faster uploads yet 99.9% as safe -- theoretical chance of losing metadata for the ~200 most recently uploaded files if there's a power-loss or your OS crashes \033[32mwal\033[0m = another 21x faster on HDDs yet 90% as safe; same pitfall as \033[33mswal\033[0m except more likely \033[32myolo\033[0m = another 1.5x faster, and removes the occasional sudden upload-pause while the disk syncs, but now you're at risk of losing the entire database in a powerloss / OS-crash profiles can be set globally (--dbd=yolo), or per-volume with volflags: -v ~/Music:music:r:c,dbd=acid """ ), ], [ "chmod", "file/folder permissions", dedent( """ global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm the value is a three- or four-digit octal number such as \033[32m755\033[0m, \033[32m0644\033[0m, \033[32m2750\033[0m, etc. if 3 digits: User, Group, Other if 4 digits: Special, User, Group, Other "Special" digit (sum of the following): \033[32m1\033[0m = sticky (files: n/a; dirs: files in directory can be deleted only by their owners) \033[32m2\033[0m = setgid (files: run executable as file group; dirs: files inherit group from directory) \033[32m4\033[0m = setuid (files: run executable as file owner; dirs: n/a) "User", "Group", "Other" digits for files: \033[32m0\033[0m = \033[35m---\033[0m = no access \033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program \033[32m2\033[0m = \033[35m-w-\033[0m = can write \033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute \033[32m4\033[0m = \033[35mr--\033[0m = can read \033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute \033[32m6\033[0m = \033[35mrw-\033[0m = can read and write \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute "User", "Group", "Other" digits for directories/folders: \033[32m0\033[0m = \033[35m---\033[0m = no access \033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents \033[32m2\033[0m = \033[35m-w-\033[0m = n/a \033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list \033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write \033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files \033[32m6\033[0m = \033[35mrw-\033[0m = n/a \033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list """ ), ], [ "pwhash", "password hashing", dedent( """ when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but copyparty will also hash and print any passwords that are non-hashed (password which do not start with '+') and then terminate afterwards if you have enabled --usernames then the password must be provided as username:password for hashing \033[36m--ah-alg\033[0m specifies the hashing algorithm and a list of optional comma-separated arguments: \033[36m--ah-alg argon2\033[0m # which is the same as: \033[36m--ah-alg argon2,3,256,4,19\033[0m use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3) \033[36m--ah-alg scrypt\033[0m # which is the same as: \033[36m--ah-alg scrypt,13,2,8,4,32\033[0m use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads, and allow using up to 32 MiB RAM (ram=cost*blksz roughly) \033[36m--ah-alg sha2\033[0m # which is the same as: \033[36m--ah-alg sha2,424242\033[0m use sha2-512 with 424242 iterations recommended: \033[32m--ah-alg argon2\033[0m (takes about 0.4 sec and 256M RAM to process a new password) argon2 needs python-package argon2-cffi, scrypt needs openssl, sha2 is always available """ ), ], [ "zm", "mDNS debugging", dedent( """ the mDNS protocol is multicast-based, which means there are thousands of fun and interesting ways for it to break unexpectedly things to check if it does not work at all: * is there a firewall blocking port 5353 on either the server or client? (for example, clients may be able to send queries to copyparty, but the replies could get lost) * is multicast accidentally disabled on either the server or client? (look for mDNS log messages saying "new client on [...]") * the router/switch must be multicast and igmp capable things to check if it works for a while but then it doesn't: * is there a firewall blocking port 5353 on either the server or client? (copyparty may be unable to see the queries from the clients, but the clients may still be able to see the initial unsolicited announce, so it works for about 2 minutes after startup until TTL expires) * does the client have multiple IPs on its interface, and some of the IPs are in subnets which the copyparty server is not a member of? for both of the above intermittent issues, try --zm-spam 30 (not spec-compliant but nothing will mind) """ ), ], ] def build_flags_desc(): ret = "" for grp, flags in flagcats.items(): ret += "\n\n\033[0m" + grp for k, v in flags.items(): v = v.replace("\n", "\n ") ret += "\n \033[36m{}\033[35m {}".format(k, v) return ret # fmt: off def add_general(ap, nc, srvname): ap2 = ap.add_argument_group("general options") ap2.add_argument("-c", metavar="PATH", type=u, default=CFG_DEF, action="append", help="\033[34mREPEATABLE:\033[0m add config file") ap2.add_argument("-nc", metavar="NUM", type=int, default=nc, help="max num clients") ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]") ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts") ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]") ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password") ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes") ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)") ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m") ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]") ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)") ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)") ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS) ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best") ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)") ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks") ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable") ap2.add_argument("--license", action="store_true", help="show licenses and exit") ap2.add_argument("--version", action="store_true", help="show versions and exit") ap2.add_argument("--versionb", action="store_true", help="show version and exit") def add_qr(ap, tty): ap2 = ap.add_argument_group("qr options") ap2.add_argument("--qr", action="store_true", help="show QR-code on startup") ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://") ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]") ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead") ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable") ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)") ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)") ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)") ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr") ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log") ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)") ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m") ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent") ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m") ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m") def add_fs(ap): ap2 = ap.add_argument_group("filesystem options") rm_re_def = "15/0.1" if ANYWIN else "0/0" ap2.add_argument("--casechk", metavar="N", type=u, default="auto", help="detect and prevent CI (case-insensitive) behavior if the underlying filesystem is CI? [\033[32my\033[0m] = detect and prevent, [\033[32mn\033[0m] = ignore and allow, [\033[32mauto\033[0m] = \033[32my\033[0m if CI fs detected. NOTE: \033[32my\033[0m is very slow but necessary for correct WebDAV behavior on Windows/Macos (volflag=casechk)") ap2.add_argument("--fsnt", metavar="OS", type=u, default="auto", help="which characters to allow in file/folder names; [\033[32mwin\033[0m] = windows (not <>:|?*\"\\/), [\033[32mmac\033[0m] = macos (not :), [\033[32mlin\033[0m] = linux (anything goes) (volflag=fsnt)") ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)") ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)") ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)") ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media") def add_share(ap): db_path = os.path.join(E.cfg, "shares.db") ap2 = ap.add_argument_group("share-url options") ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]") ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in") ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)") ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share") ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week") ap2.add_argument("--shr-site", metavar="URL", type=u, default="--site", help="public URL to assume when creating share-links; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--shr-v", action="store_true", help="debug") def add_upload(ap): ap2 = ap.add_argument_group("upload options") ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m") ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip") ap2.add_argument("--up-site", metavar="URL", type=u, default="--site", help="public URL to assume when creating links to uploaded files; example: [\033[32mhttps://example.com/\033[0m]") ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)") ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)") ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)") ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h") ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)") ap2.add_argument("--apnd-who", metavar="NUM", type=u, default="dw", help="who can append to existing files? [\033[32mno\033[0m]=nobody, [\033[32maw\033[0m]=admin+write, [\033[32mdw\033[0m]=delete+write, [\033[32mw\033[0m]=write (volflag=apnd_who)") ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)") ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600") ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)") ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)") ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)") ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m2750\033[0m] = setgid + owner-RW + group-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("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)") ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)") ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)") ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)") ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)") ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)") ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)") ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)") ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)") ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually") ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash") ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)") ap2.add_argument("--rm-partial", action="store_true", help="delete the .PARTIAL file when an unfinished upload expires after \033[33m--snap-drop\033[0m (volflag=rm_partial)") ap2.add_argument("--u2ts", metavar="TXT", type=u, default="c", help="how to timestamp uploaded files; [\033[32mc\033[0m]=client-last-modified, [\033[32mu\033[0m]=upload-time, [\033[32mfc\033[0m]=force-c, [\033[32mfu\033[0m]=force-u (volflag=u2ts)") ap2.add_argument("--rotf-tz", metavar="TXT", type=u, default="UTC", help="default timezone for the rotf upload rule; examples: [\033[32mEurope/Oslo\033[0m], [\033[32mAmerica/Toronto\033[0m], [\033[32mAntarctica/South_Pole\033[0m] (volflag=rotf_tz)") ap2.add_argument("--rand", action="store_true", help="force randomized filenames, \033[33m--nrand\033[0m chars long (volflag=rand)") ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)") ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)") ap2.add_argument("--df", metavar="GiB", type=u, default="0", help="ensure \033[33mGiB\033[0m free disk space by rejecting upload requests; assumes gigabytes unless a unit suffix is given: [\033[32m256m\033[0m], [\033[32m4\033[0m], [\033[32m2T\033[0m] (volflag=df)") ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files") ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; [\033[32m-1\033[0m] = forbidden/always-off, [\033[32m0\033[0m] = default-off and warn if enabled, [\033[32m1\033[0m] = default-off, [\033[32m2\033[0m] = on, [\033[32m3\033[0m] = on and disable datecheck") ap2.add_argument("--nosubtle", metavar="N", type=int, default=0, help="when to use a wasm-hasher instead of the browser's builtin; faster on chrome, but buggy in older chrome versions. [\033[32m0\033[0m] = only when necessary (non-https), [\033[32m1\033[0m] = always (all browsers), [\033[32m2\033[0m] = always on chrome/firefox, [\033[32m3\033[0m] = always on chrome, [\033[32mN\033[0m] = chrome-version N and newer (recommendation: 137)") ap2.add_argument("--u2j", metavar="JOBS", type=int, default=2, help="web-client: number of file chunks to upload in parallel; 1 or 2 is good when latency is low (same-country), 2~4 for android-clients, 2~6 for cross-atlantic. Max is 6 in most browsers. Big values increase network-speed but may reduce HDD-speed") ap2.add_argument("--u2sz", metavar="N,N,N", type=u, default="1,64,96", help="web-client: default upload chunksize (MiB); sets \033[33mmin,default,max\033[0m in the settings gui. Each HTTP POST will aim for \033[33mdefault\033[0m, and never exceed \033[33mmax\033[0m. Cloudflare max is 96. Big values are good for cross-atlantic but may increase HDD fragmentation on some FS. Disable this optimization with [\033[32m1,1,1\033[0m]") ap2.add_argument("--u2ow", metavar="NUM", type=int, default=0, help="web-client: default setting for when to replace/overwrite existing files; [\033[32m0\033[0m]=never, [\033[32m1\033[0m]=if-client-newer, [\033[32m2\033[0m]=always (volflag=u2ow)") ap2.add_argument("--u2sort", metavar="TXT", type=u, default="s", help="upload order; [\033[32ms\033[0m]=smallest-first, [\033[32mn\033[0m]=alphabetical, [\033[32mfs\033[0m]=force-s, [\033[32mfn\033[0m]=force-n -- alphabetical is a bit slower on fiber/LAN but makes it easier to eyeball if everything went fine") ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory") def add_network(ap): ap2 = ap.add_argument_group("network options") ap2.add_argument("-i", metavar="IP", type=u, default="::", help="IPs and/or unix-sockets to listen on (comma-separated list; see \033[33m--help-bind\033[0m). Default: all IPv4 and IPv6") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to listen on (comma/range); ignored for unix-sockets") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 in mDNS replies, even if the NIC has routable IPs (breaks some mDNS clients)") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=9999999, help="which ip to associate clients with; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd, unsafe), [\033[32m-1\033[0m]=closest-proxy, [\033[32m-2\033[0m]=second-hop, [\033[32m-3\033[0m]=third-hop") ap2.add_argument("--xff-hdr", metavar="NAME", type=u, default="x-forwarded-for", help="if reverse-proxied, which http header to read the client's real ip from") ap2.add_argument("--xf-host", metavar="NAME", type=u, default="x-forwarded-host", help="if reverse-proxied, which http header to read the correct Host value from; this header must contain the server's external domain name") ap2.add_argument("--xf-proto", metavar="NAME", type=u, default="x-forwarded-proto", help="if reverse-proxied, which http header to read the correct protocol value from; this header must contain either 'http' or 'https'") ap2.add_argument("--xf-proto-fb", metavar="T", type=u, default="", help="protocol to assume if the X-Forwarded-Proto header (\033[33m--xf-proto\033[0m) is not provided by the reverseproxy; either 'http' or 'https'") ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)") ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]\n └─for performance and security, this only looks at the TCP/Network-level IP, and will NOT work behind a reverseproxy") ap2.add_argument("--ipar", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated).\n └─this is reverseproxy-compatible; reads client-IP from 'X-Forwarded-For' if possible, with TCP/Network IP as fallback") ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]") ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)") ap2.add_argument("--http-vary", metavar="TXT", type=u, default="Origin, PW, Cookie", help="value of the 'Vary' response-header; a hint for caching proxies") ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets") if ANYWIN: ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances") elif not MACOS: ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)") ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started") ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started") ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)") ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost") ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)") ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes") ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds") ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0.0, help="debug: response delay in seconds") ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0.0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m") def add_tls(ap, cert_path): ap2 = ap.add_argument_group("SSL/TLS options") ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext") ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls") ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain") ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe") ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") ap2.add_argument("--ssl-log", metavar="PATH", type=u, default="", help="log master secrets for later decryption in wireshark") def add_cert(ap, cert_path): cert_dir = os.path.dirname(cert_path) ap2 = ap.add_argument_group("TLS certificate generator options") ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation") ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate") ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each \033[33m--crt-ns\033[0m") ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert") ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert") ap2.add_argument("--crt-nohn", action="store_true", help="do not add mDNS names / hostname into cert") ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert") ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650.0, help="ca-certificate expiration time in days") ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365.0, help="server-cert expiration time in days") ap2.add_argument("--crt-cn", metavar="TXT", type=u, default="partyco", help="CA/server-cert common-name") ap2.add_argument("--crt-cnc", metavar="TXT", type=u, default="--crt-cn", help="override CA name") ap2.add_argument("--crt-cns", metavar="TXT", type=u, default="--crt-cn cpp", help="override server-cert name") ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72.0, help="backdate in hours") ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: \033[32mecdsa-256 rsa-4096 rsa-2048\033[0m") def add_auth(ap): idp_db = os.path.join(E.cfg, "idp.db") ses_db = os.path.join(E.cfg, "sessions.db") ap2 = ap.add_argument_group("IdP / identity provider / user authentication options") ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp") ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") ap2.add_argument("--idp-chsub", metavar="TXT", type=u, default="", help="characters to replace in usernames/groupnames; a list of pairs of characters separated by | so for example | _| will replace spaces with _ to make configuration easier, or |%%_|^_|@_| will replace %%/^/@ with _") ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)") ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page") ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button") ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m") ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord") ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients") ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)") ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies") ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known") ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]") ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]") ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS) def add_chpw(ap): db_path = os.path.join(E.cfg, "chpw.json") ap2 = ap.add_argument_group("user-changeable passwords options") ap2.add_argument("--chpw", action="store_true", help="allow users to change their own passwords") ap2.add_argument("--chpw-no", metavar="U,U,U", type=u, action="append", help="\033[34mREPEATABLE:\033[0m do not allow password-changes for this comma-separated list of usernames") ap2.add_argument("--chpw-db", metavar="PATH", type=u, default=db_path, help="where to store the passwords database (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--chpw-len", metavar="N", type=int, default=8, help="minimum password length") ap2.add_argument("--chpw-v", metavar="LVL", type=int, default=2, help="verbosity of summary on config load [\033[32m0\033[0m] = nothing at all, [\033[32m1\033[0m] = number of users, [\033[32m2\033[0m] = list users with default-pw, [\033[32m3\033[0m] = list all users") def add_zeroconf(ap): ap2 = ap.add_argument_group("Zeroconf options") ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)") ap2.add_argument("--z-on", metavar="NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes\n └─example: \033[32meth0, wlo1, virhost0, 192.168.123.0/24, fd00:fda::/96\033[0m") ap2.add_argument("--z-off", metavar="NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--z-chk", metavar="SEC", type=int, default=10, help="check for network changes every \033[33mSEC\033[0m seconds (0=disable)") ap2.add_argument("-zv", action="store_true", help="verbose all zeroconf backends") ap2.add_argument("--mc-hop", metavar="SEC", type=int, default=0, help="rejoin multicast groups every \033[33mSEC\033[0m seconds (workaround for some switches/routers which cause mDNS to suddenly stop working after some time); try [\033[32m300\033[0m] or [\033[32m180\033[0m]\n └─note: can be due to firewalls; make sure UDP port 5353 is open in both directions (on clients too)") def add_zc_mdns(ap): ap2 = ap.add_argument_group("Zeroconf-mDNS options; also see --help-zm") ap2.add_argument("--zm", action="store_true", help="announce the enabled protocols over mDNS (multicast DNS-SD) -- compatible with KDE, gnome, macOS, ...") ap2.add_argument("--zm-on", metavar="NETS", type=u, default="", help="enable mDNS ONLY on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--zm-off", metavar="NETS", type=u, default="", help="disable mDNS on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--zm4", action="store_true", help="IPv4 only -- try this if some clients can't connect") ap2.add_argument("--zm6", action="store_true", help="IPv6 only") ap2.add_argument("--zmv", action="store_true", help="verbose mdns") ap2.add_argument("--zmvv", action="store_true", help="verboser mdns") ap2.add_argument("--zm-http", metavar="PORT", type=int, default=-1, help="port to announce for http/webdav; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649") ap2.add_argument("--zm-https", metavar="PORT", type=int, default=-1, help="port to announce for https/webdavs; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649") ap2.add_argument("--zm-no-pe", action="store_true", help="mute parser errors (invalid incoming MDNS packets)") ap2.add_argument("--zm-nwa-1", action="store_true", help="disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)") ap2.add_argument("--zms", metavar="dhf", type=u, default="", help="list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (\033[32mDdfs\033[0m if \033[33m--ftp\033[0m and \033[33m--smb\033[0m is set, \033[32mDd\033[0m otherwise)") ap2.add_argument("--zm-ld", metavar="PATH", type=u, default="", help="link a specific folder for webdav shares") ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares") ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares") ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares") ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems") ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network") ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not") ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty") ap2.add_argument("--zm-spam", metavar="SEC", type=float, default=0.0, help="send unsolicited announce every \033[33mSEC\033[0m; useful if clients have IPs in a subnet which doesn't overlap with the server, or to avoid some firewall issues") def add_zc_ssdp(ap): ap2 = ap.add_argument_group("Zeroconf-SSDP options") ap2.add_argument("--zs", action="store_true", help="announce the enabled protocols over SSDP -- compatible with Windows") ap2.add_argument("--zs-on", metavar="NETS", type=u, default="", help="enable SSDP ONLY on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--zs-off", metavar="NETS", type=u, default="", help="disable SSDP on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--zsv", action="store_true", help="verbose SSDP") ap2.add_argument("--zsl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] (goes directly to /priv/ with password hunter2) or [\033[32m?hc=priv&pw=hunter2\033[0m] (shows mounting options for /priv/ with password)") ap2.add_argument("--zsid", metavar="UUID", type=u, default=zsid, help="USN (device identifier) to announce") def add_sftp(ap): ap2 = ap.add_argument_group("SFTP options") ap2.add_argument("--sftp", metavar="PORT", type=int, default=0, help="enable SFTP server on \033[33mPORT\033[0m, for example \033[32m3922") ap2.add_argument("--sftpv", action="store_true", help="verbose") ap2.add_argument("--sftpvv", action="store_true", help="verboser") ap2.add_argument("--sftp-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol") ap2.add_argument("--sftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--sftp-key", metavar="U K", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add ssh-key \033[33mK\033[0m for user \033[33mU\033[0m (username, space, key-type, space, base64); if user has multiple keys, then repeat this option for each key\n └─commandline example: --sftp-key 'david ssh-ed25519 AAAAC3NzaC...'\n └─config-file example: sftp-key: david ssh-ed25519 AAAAC3NzaC...") ap2.add_argument("--sftp-key2u", action="append", help=argparse.SUPPRESS) ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)") ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username") ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated") ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath") ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") def add_ftp(ap): ap2 = ap.add_argument_group("FTP options (TCP only)") ap2.add_argument("--ftp", metavar="PORT", type=int, default=0, help="enable FTP server on \033[33mPORT\033[0m, for example \033[32m3921") ap2.add_argument("--ftps", metavar="PORT", type=int, default=0, help="enable FTPS server on \033[33mPORT\033[0m, for example \033[32m3990") ap2.add_argument("--ftpv", action="store_true", help="verbose") ap2.add_argument("--ftp-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol") ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--ftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--ftp-no-ow", action="store_true", help="if target file exists, reject upload instead of overwrite") ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than \033[33mSEC\033[0m seconds ago)") ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, default="", help="the NAT address to use for passive connections") ap2.add_argument("--ftp-pr", metavar="P-P", type=u, default="", help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000") def add_webdav(ap): ap2 = ap.add_argument_group("WebDAV options") ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. Some webdav clients need this option for editing existing files; not necessary for clients that send the 'x-oc-mtime' header. Regardless, the delete-permission must always be given. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)") ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this") ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)") ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)") ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)") ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of user-agents which ARE webdav-clients, and expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank") ap2.add_argument("--dav-port", metavar="P", type=int, default=0, help="additional port to listen on for misbehaving webdav clients which pretend they are graphical browsers; an alternative/supplement to dav-ua1") ap2.add_argument("--ua-nodav", metavar="PTN", type=u, default=r"^(Mozilla/|NetworkingExtension/|com\.apple\.WebKit)", help="regex of user-agents which are NOT webdav-clients") ap2.add_argument("--p-nodav", metavar="P,P", type=u, default="", help="server-ports (comma-sep.) which are NOT webdav-clients; an alternative/supplement to ua-nodav") def add_tftp(ap): ap2 = ap.add_argument_group("TFTP options (UDP only)") ap2.add_argument("--tftp", metavar="PORT", type=int, default=0, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969") ap2.add_argument("--tftp-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol") ap2.add_argument("--tftp4", action="store_true", help="only listen on IPv4") ap2.add_argument("--tftpv", action="store_true", help="verbose") ap2.add_argument("--tftpvv", action="store_true", help="verboser") ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations") ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...") ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing") ap2.add_argument("--tftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]") ap2.add_argument("--tftp-pr", metavar="P-P", type=u, default="", help="the range of UDP ports to use for data transfer, for example \033[32m12000-13000") def add_smb(ap): ap2 = ap.add_argument_group("SMB/CIFS options") ap2.add_argument("--smb", action="store_true", help="enable smb (read-only) -- this requires running copyparty as root on linux and macos unless \033[33m--smb-port\033[0m is set above 1024 and your OS does port-forwarding from 445 to that.\n\033[1;31mWARNING:\033[0m this protocol is DANGEROUS and buggy! Never expose to the internet!") ap2.add_argument("--smb-i", metavar="IP", type=u, default="-i", help="IPs to listen on (comma-separated list). Set this to override \033[33m-i\033[0m for this protocol") ap2.add_argument("--smbw", action="store_true", help="enable write support (please dont)") ap2.add_argument("--smb1", action="store_true", help="disable SMBv2, only enable SMBv1 (CIFS)") ap2.add_argument("--smb-port", metavar="PORT", type=int, default=445, help="port to listen on -- if you change this value, you must NAT from TCP:445 to this port using iptables or similar") ap2.add_argument("--smb-nwa-1", action="store_true", help="truncate directory listings to 64kB (~400 files); avoids impacket-0.11 bug, fixes impacket-0.12 performance") ap2.add_argument("--smb-nwa-2", action="store_true", help="disable impacket workaround for filecopy globs") ap2.add_argument("--smba", action="store_true", help="small performance boost: disable per-account permissions, enables account coalescing instead (if one user has write/delete-access, then everyone does)") ap2.add_argument("--smbv", action="store_true", help="verbose") ap2.add_argument("--smbvv", action="store_true", help="verboser") ap2.add_argument("--smbvvv", action="store_true", help="verbosest") def add_opds(ap): ap2 = ap.add_argument_group("OPDS options") ap2.add_argument("--opds", action="store_true", help="enable opds -- allows e-book readers to browse and download files (volflag=opds)") ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)") def add_handlers(ap): ap2 = ap.add_argument_group("handlers (see --help-handlers)") ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 404s by executing \033[33mPY\033[0m file") ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="\033[34mREPEATABLE:\033[0m handle 403s by executing \033[33mPY\033[0m file") ap2.add_argument("--hot-handlers", action="store_true", help="recompile handlers on each request -- expensive but convenient when hacking on stuff") def add_hooks(ap): ap2 = ap.add_argument_group("event hooks (see --help-hooks)") ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file upload starts") ap2.add_argument("--xau", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file upload finishes") ap2.add_argument("--xiu", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after all uploads finish and volume is idle") ap2.add_argument("--xbc", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file copy") ap2.add_argument("--xac", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file copy") ap2.add_argument("--xbr", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file move/rename") ap2.add_argument("--xar", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file move/rename") ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m before a file delete") ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m after a file delete") ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m on message") ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="\033[34mREPEATABLE:\033[0m execute \033[33mCMD\033[0m if someone gets banned (pw/404/403/url)") ap2.add_argument("--hook-v", action="store_true", help="verbose hooks") def add_stats(ap): ap2 = ap.add_argument_group("grafana/prometheus metrics endpoint") ap2.add_argument("--stats", action="store_true", help="enable openmetrics at /.cpr/metrics for admin accounts") ap2.add_argument("--stats-u", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to access /.cpr/metrics even if they aren't admin") ap2.add_argument("--nos-hdd", action="store_true", help="disable disk-space metrics (used/free space)") ap2.add_argument("--nos-vol", action="store_true", help="disable volume size metrics (num files, total bytes, vmaxb/vmaxn)") ap2.add_argument("--nos-vst", action="store_true", help="disable volume state metrics (indexing, analyzing, activity)") ap2.add_argument("--nos-dup", action="store_true", help="disable dupe-files metrics (good idea; very slow)") ap2.add_argument("--nos-unf", action="store_true", help="disable unfinished-uploads metrics") def add_yolo(ap): ap2 = ap.add_argument_group("yolo options") ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests") ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF") ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets") def add_optouts(ap): ap2 = ap.add_argument_group("opt-outs") ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)") ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection in the terminal window, as this would pause execution)") ap2.add_argument("--no-dav", action="store_true", help="disable webdav support") ap2.add_argument("--no-del", action="store_true", help="disable delete operations") ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations") ap2.add_argument("--no-cp", action="store_true", help="disable copy operations") ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move") ap2.add_argument("-nih", action="store_true", help="no info hostname -- removes it from the UI corner, but the value of \033[33m--bname\033[0m still shows in the browsertab title") ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as \033[33m--du-who no\033[0m") ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI") ap2.add_argument("--smsg", metavar="T,T", type=u, default="POST", help="HTTP-methods to allow ?smsg for; will execute xm hooks like urlform / message-to-serverlog; dangerous example: [\033[32mGET,POST\033[0m]. \033[1;31mWARNING:\033[0m The default (POST) is safe, but GET is dangerous; security/CSRF hazard") ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)") ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)") ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)") ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)") ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume") ap2.add_argument("--ua-nozip", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from download-as-zip/tar; disable with [\033[32mno\033[0m] or blank") ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m") ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)") ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time") ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)") ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)") ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)") ap2.add_argument("--no-zls", action="store_true", help="disable browsing the contents of zip/cbz files, does not affect thumbnails") def add_safety(ap): ap2 = ap.add_argument_group("safety options") ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js") ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --reflink --dav-auth --vague-403 -nih") ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r") ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]") ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)") ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)") ap2.add_argument("--vol-nospawn", action="store_true", help="if a volume's folder does not exist on the HDD, then do not create it (continue with warning) (volflag=nospawn)") ap2.add_argument("--vol-or-crash", action="store_true", help="if a volume's folder does not exist on the HDD, then burst into flames (volflag=assert_root)") ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles") ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings") ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV") ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--dont-ban", metavar="TXT", type=u, default="no", help="anyone at this accesslevel or above will not get banned: [\033[32mav\033[0m]=admin-in-volume, [\033[32maa\033[0m]=has-admin-anywhere, [\033[32mrw\033[0m]=read-write, [\033[32mauth\033[0m]=authenticated, [\033[32many\033[0m]=disable-all-bans, [\033[32mno\033[0m]=anyone-can-get-banned") ap2.add_argument("--banmsg", metavar="TXT", type=u, default="thank you for playing \u00a0 (see fileserver log and readme)", help="the response to send to banned users; can be @ban.html to send the contents of ban.html") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h") ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month") ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)") ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; applies only to permissions g/G/h (decent replacement for \033[33m--ban-404\033[0m if that can't be used)") ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\..{3}|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 403/404-bans; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy") ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies") ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header") ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0") ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]") ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives") ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)") def add_salt(ap, fk_salt, dk_salt, ah_salt): ap2 = ap.add_argument_group("salting options") ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: \033[32margon2 scrypt sha2 none\033[0m (each optionally followed by alg-specific comma-sep. config)") ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if \033[33m--ah-alg\033[0m is none (default)") ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]") ap2.add_argument("--ah-cli", action="store_true", help="launch an interactive shell which hashes passwords without ever storing or displaying the original passwords") ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files") ap2.add_argument("--dk-salt", metavar="SALT", type=u, default=dk_salt, help="per-directory accesskey salt; used to generate unpredictable URLs to share folders with users who only have the 'get' permission") ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)") ap2.add_argument("--show-ah-salt", action="store_true", help="on startup, print the effective value of \033[33m--ah-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)") ap2.add_argument("--show-fk-salt", action="store_true", help="on startup, print the effective value of \033[33m--fk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)") ap2.add_argument("--show-dk-salt", action="store_true", help="on startup, print the effective value of \033[33m--dk-salt\033[0m (the autogenerated value in $XDG_CONFIG_HOME unless otherwise specified)") def add_shutdown(ap): ap2 = ap.add_argument_group("shutdown options") ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints") ap2.add_argument("--ign-ebind-all", action="store_true", help="continue running even if it's impossible to receive connections at all") ap2.add_argument("--exit", metavar="WHEN", type=u, default="", help="shutdown after \033[33mWHEN\033[0m has finished; [\033[32mcfg\033[0m] config parsing, [\033[32midx\033[0m] volscan + multimedia indexing") def add_logging(ap): ap2 = ap.add_argument_group("logging options") ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages") ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)") ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals") ap2.add_argument("--log-date", metavar="TXT", type=u, default="", help="date-format, for example [\033[32m%%Y-%%m-%%d\033[0m] (default is disabled; no date, just HH:MM:SS)") ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed") ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[xwjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m") ap2.add_argument("--scan-st-r", metavar="SEC", type=float, default=0.1, help="fs-indexing: wait \033[33mSEC\033[0m between each status-message") ap2.add_argument("--scan-pr-r", metavar="SEC", type=float, default=10, help="fs-indexing: wait \033[33mSEC\033[0m between each 'progress:' message") ap2.add_argument("--scan-pr-s", metavar="MiB", type=float, default=1, help="fs-indexing: say 'file: ' when a file larger than \033[33mMiB\033[0m is about to be hashed") def add_admin(ap): ap2 = ap.add_argument_group("admin panel options") ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)") ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)") ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks); same as --stack-who=no") ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)") ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel") ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone") ap2.add_argument("--ups-who", metavar="LVL", type=int, default=2, help="who can see recent uploads on the ?ru page? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone (volflag=ups_who)") ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins") ap2.add_argument("--stack-who", metavar="LVL", type=u, default="a", help="who can see the ?stack page (list of threads)? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admins, [\033[32mrw\033[0m]=read+write, [\033[32mall\033[0m]=everyone") ap2.add_argument("--stack-v", action="store_true", help="verbose ?stack") def add_thumbnail(ap): th_ram = (RAM_AVAIL or RAM_TOTAL or 9) * 0.6 th_ram = int(max(min(th_ram, 6), 0.3) * 10) / 10 ap2 = ap.add_argument_group("thumbnail options") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)") ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)") ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)") ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="webp/jpg thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)") ap2.add_argument("--th-qvx", metavar="N", type=int, default=64, help="jxl thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qvx)") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,raw,pil,ff", help="image decoders, in order of preference") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output") ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)") ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)") ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds") ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec") # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips # https://stackoverflow.com/a/47612661 # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow") ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="3fr,avif,cr2,cr3,crw,dcr,dng,erf,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,k25,mdc,mef,mrw,nef,nii,pfm,pgm,png,ppm,raf,raw,sr2,srf,svg,tif,tiff,webp,x3f", help="image formats to decode using pyvips") ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="3fr,arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mdc,mef,mos,mrw,nef,nrw,orf,pef,raf,raw,sr2,srf,srw,x3f", help="image formats to decode using rawpy") ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg") ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)") ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg") def add_transcoding(ap): ap2 = ap.add_argument_group("transcoding options") ap2.add_argument("--q-opus", metavar="KBPS", type=int, default=128, help="target bitrate for transcoding to opus; set 0 to disable") ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable") ap2.add_argument("--allow-wav", action="store_true", help="allow transcoding to wav (lossless, uncompressed)") ap2.add_argument("--allow-flac", action="store_true", help="allow transcoding to flac (lossless, compressed)") ap2.add_argument("--no-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead") ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead") ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding") ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)") ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds") def add_tail(ap): ap2 = ap.add_argument_group("tailing options (realtime streaming of a growing file)") ap2.add_argument("--tail-who", metavar="LVL", type=int, default=2, help="who can tail? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=tail_who)") ap2.add_argument("--tail-cmax", metavar="N", type=int, default=64, help="do not allow starting a new tail if more than \033[33mN\033[0m active downloads") ap2.add_argument("--tail-tmax", metavar="SEC", type=float, default=0, help="terminate connection after \033[33mSEC\033[0m seconds; [\033[32m0\033[0m]=never (volflag=tail_tmax)") ap2.add_argument("--tail-rate", metavar="SEC", type=float, default=0.2, help="check for new data every \033[33mSEC\033[0m seconds (volflag=tail_rate)") ap2.add_argument("--tail-ka", metavar="SEC", type=float, default=3.0, help="send a zerobyte if connection is idle for \033[33mSEC\033[0m seconds to prevent disconnect") ap2.add_argument("--tail-fd", metavar="SEC", type=float, default=1.0, help="check if file was replaced (new fd) if idle for \033[33mSEC\033[0m seconds (volflag=tail_fd)") def add_rss(ap): ap2 = ap.add_argument_group("RSS options") ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)") ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')") ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all") ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files (volflag=rss_sort)") ap2.add_argument("--rss-fmt-t", metavar="TXT", type=u, default="{fname}", help="title format (url-param 'rss_fmt_t') (volflag=rss_fmt_t)") ap2.add_argument("--rss-fmt-d", metavar="TXT", type=u, default="{artist} - {title}", help="description format (url-param 'rss_fmt_d') (volflag=rss_fmt_d)") def add_db_general(ap, hcores): noidx = APPLESAN_TXT if MACOS else "" ap2 = ap.add_argument_group("general db options") ap2.add_argument("-e2d", action="store_true", help="enable up2k database; this enables file search, upload-undo, improves deduplication") ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets \033[33m-e2d\033[0m") ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets \033[33m-e2ds\033[0m") ap2.add_argument("-e2v", action="store_true", help="verify file integrity; rehash all files and compare with db") ap2.add_argument("-e2vu", action="store_true", help="on hash mismatch: update the database with the new hash") ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty") ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)") ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)") ap2.add_argument("--fika", metavar="TXT", type=u, default="ucd", help="list of user-actions to allow while filesystem-indexer is still busy; set blank to never interrupt indexing (old default). [\033[32mu\033[0m]=uploads, [\033[32mc\033[0m]=filecopy, [\033[32mm\033[0m]=move/rename, [\033[32md\033[0m]=delete. NOTE: [\033[32mm\033[0m] is untested/scary, and blank is recommended if dedup enabled") ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)") ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)") ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)") ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch") ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower") ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)") ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)") ap2.add_argument("--forget-ip", metavar="MIN", type=int, default=0, help="remove uploader-IP from database (and make unpost impossible) \033[33mMIN\033[0m minutes after upload, for GDPR reasons. Default [\033[32m0\033[0m] is never-forget. [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month. (volflag=forget_ip)") ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)") ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (probably buggy, not recommended) (volflag=xlink)") ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing") ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)") ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)") ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags") ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds") ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially") ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)") ap2.add_argument("--dotsrch", action="store_true", help="show dotfiles in search results (volflags: dotsrch | nodotsrch)") def add_db_metadata(ap): ap2 = ap.add_argument_group("metadata db options") ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...") ap2.add_argument("-e2ts", action="store_true", help="scan newly discovered files for metadata on startup; sets \033[33m-e2t\033[0m") ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets \033[33m-e2ts\033[0m") ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will detect more tags") ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader; is probably safer") ap2.add_argument("--mtag-to", metavar="SEC", type=int, default=60, help="timeout for FFprobe tag-scan") ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for tag scanning") ap2.add_argument("--mtag-v", action="store_true", help="verbose tag scanning; print errors from mtp subprocesses and such") ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/FFprobe parsers") ap2.add_argument("--db-xattr", metavar="t,t", type=u, default="", help="read file xattrs as metadata tags; [\033[32ma,b\033[0m] reads keys \033[33ma\033[0m and \033[33mb\033[0m as tags \033[33ma\033[0m and \033[33mb\033[0m, [\033[32ma=foo,b=bar\033[0m] reads keys \033[33ma\033[0m and \033[33mb\033[0m as tags \033[33mfoo\033[0m and \033[33mbar\033[0m, [\033[32m~~a,b\033[0m] does everything except \033[33ma\033[0m and \033[33mb\033[0m, [\033[32m~~\033[0m] does everything. NOTE: Each tag must also be enabled with \033[33m-mte\033[0m (volflag=db_xattr)") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add/replace metadata mapping") ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.); either an entire replacement list, or add/remove stuff on the default-list with +foo or /bar", default=DEF_MTE) ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.); assign/add/remove same as \033[33m-mte\033[0m", default=DEF_MTH) ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m read tag \033[33mM\033[0m using program \033[33mBIN\033[0m to parse the file") ap2.add_argument("--have-db-xattr", action="store_true", help=argparse.SUPPRESS) def add_txt(ap): ap2 = ap.add_argument_group("textfile options") ap2.add_argument("--rw-edit", metavar="T,T", type=u, default="md", help="comma-sep. list of file-extensions to allow editing with permissions read+write; all others require read+write+delete (volflag=rw_edit)") ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)") ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)") ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)") ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds") ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk") ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)") ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)") ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)") ap2.add_argument("--ua-nodoc", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from viewing documents through ?doc=[...]; disable with [\033[32mno\033[0m] or blank") def add_og(ap): ap2 = ap.add_argument_group("og / open graph / discord-embed options") ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)") ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)") ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)") ap2.add_argument("--og-no-head", action="store_true", help="do not automatically add OG entries into (useful if you're doing this yourself in a template or such) (volflag=og_no_head)") ap2.add_argument("--og-th", metavar="FMT", type=u, default="jf3", help="thumbnail format; j=jpeg, jf=jpeg-uncropped, jf3=jpeg-uncropped-large, w=webm, ... (volflag=og_th)") ap2.add_argument("--og-title", metavar="TXT", type=u, default="", help="fallback title if there is nothing in the \033[33m-e2t\033[0m database (volflag=og_title)") ap2.add_argument("--og-title-a", metavar="T", type=u, default="🎵 {{ artist }} - {{ title }}", help="audio title format; takes any metadata key (volflag=og_title_a)") ap2.add_argument("--og-title-v", metavar="T", type=u, default="{{ title }}", help="video title format; takes any metadata key (volflag=og_title_v)") ap2.add_argument("--og-title-i", metavar="T", type=u, default="{{ title }}", help="image title format; takes any metadata key (volflag=og_title_i)") ap2.add_argument("--og-s-title", action="store_true", help="force default title; do not read from tags (volflag=og_s_title)") ap2.add_argument("--og-desc", metavar="TXT", type=u, default="", help="description text; same for all files, disable with [\033[32m-\033[0m] (volflag=og_desc)") ap2.add_argument("--og-site", metavar="TXT", type=u, default="", help="sitename; defaults to \033[33m--name\033[0m, disable with [\033[32m-\033[0m] (volflag=og_site)") ap2.add_argument("--tcolor", metavar="RGB", type=u, default="333", help="accent color (3 or 6 hex digits); may also affect safari and/or android-chrome (volflag=tcolor)") ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m") def add_ui(ap, retry: int): THEMES = 10 ap2 = ap.add_argument_group("ui options") ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)") ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)") ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,)) ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed") ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent") ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)") ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)") ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)") ap2.add_argument("--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("--dlni", action="store_true", help="force download (don't show inline) when files are clicked (volflag:dlni)") 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("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)") ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m 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", type=u, default="", help=argparse.SUPPRESS) ap2.add_argument("--notooltips", action="store_true", help="tooltips disabled as default") ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]") ap2.add_argument("--css-browser", metavar="L", type=u, default="", help="URL to additional CSS to include in the filebrowser html") ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html") ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages") ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the of all HTML pages (except for basic-browser); can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)") ap2.add_argument("--html-head-s", metavar="T", type=u, default="", help="text to append to the of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)") ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)") ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext") ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)") ap2.add_argument("--prologues", metavar="T,T", type=u, default=".prologue.html", help="comma-sep. list of filenames to scan for and use as prologues (embed above/before directory listing) (volflag=prologues)") ap2.add_argument("--epilogues", metavar="T,T", type=u, default=".epilogue.html", help="comma-sep. list of filenames to scan for and use as epilogues (embed below/after directory listing) (volflag=epilogues)") ap2.add_argument("--preadmes", metavar="T,T", type=u, default="preadme.md,PREADME.md", help="comma-sep. list of filenames to scan for and use as preadmes (embed above/before directory listing) (volflag=preadmes)") ap2.add_argument("--readmes", metavar="T,T", type=u, default="readme.md,README.md", help="comma-sep. list of filenames to scan for and use as readmes (embed below/after directory listing) (volflag=readmes)") ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents") ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)") ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m") ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all") ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone") ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)") ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS) ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS) ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on") ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once") ap2.add_argument("--md-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for README.md docs (volflag=md_sbf); see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox") ap2.add_argument("--lg-sbf", metavar="FLAGS", type=u, default="downloads forms popups scripts top-navigation-by-user-activation", help="list of capabilities to allow in the iframe 'sandbox' attribute for prologue/epilogue docs (volflag=lg_sbf)") ap2.add_argument("--md-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for README.md docs, for example [\033[32mfullscreen\033[0m] (volflag=md_sba)") ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes") ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)") ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support") ap2.add_argument("--ui-nombar", action="store_true", help="hide top-menu in the UI (volflag=ui_nombar)") ap2.add_argument("--ui-noacci", action="store_true", help="hide account-info in the UI (volflag=ui_noacci)") ap2.add_argument("--ui-nosrvi", action="store_true", help="hide server-info in the UI (volflag=ui_nosrvi)") ap2.add_argument("--ui-nonav", action="store_true", help="hide navpane+breadcrumbs (volflag=ui_nonav)") ap2.add_argument("--ui-notree", action="store_true", help="hide navpane in the UI (volflag=ui_notree)") ap2.add_argument("--ui-nocpla", action="store_true", help="hide cpanel-link in the UI (volflag=ui_nocpla)") ap2.add_argument("--ui-nolbar", action="store_true", help="hide link-bar in the UI (volflag=ui_nolbar)") ap2.add_argument("--ui-noctxb", action="store_true", help="hide context-buttons in the UI (volflag=ui_noctxb)") ap2.add_argument("--ui-norepl", action="store_true", help="hide repl-button in the UI (volflag=ui_norepl)") ap2.add_argument("--have-unlistc", action="store_true", help=argparse.SUPPRESS) def add_debug(ap): ap2 = ap.add_argument_group("debug options") ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)") ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)") ap2.add_argument("--deps", action="store_true", help="list information about detected optional dependencies") if hasattr(select, "poll"): ap2.add_argument("--no-poll", action="store_true", help="kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)") ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead") ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead") ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests") ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind") ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks") ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc") ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60") ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m") ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files") ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m") ap2.add_argument("--bf-nc", metavar="NUM", type=int, default=200, help="bak-flips: stop if there's more than \033[33mNUM\033[0m files at \033[33m--kf-dir\033[0m already; default: 6.3 GiB max (200*32M)") ap2.add_argument("--bf-dir", metavar="PATH", type=u, default="bf", help="bak-flips: store corrupted chunks at \033[33mPATH\033[0m; default: folder named 'bf' wherever copyparty was started") ap2.add_argument("--bf-log", metavar="PATH", type=u, default="", help="bak-flips: log corruption info to a textfile at \033[33mPATH\033[0m") ap2.add_argument("--no-cfg-cmt-warn", action="store_true", help=argparse.SUPPRESS) # fmt: on def run_argparse( argv: list[str], formatter: Any, retry: int, nc: int, verbose=True ) -> argparse.Namespace: ap = argparse.ArgumentParser( formatter_class=formatter, usage=argparse.SUPPRESS, prog="copyparty", description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT), ) cert_path = os.path.join(E.cfg, "cert.pem") fk_salt = get_salt("fk", 18) dk_salt = get_salt("dk", 30) ah_salt = get_salt("ah", 18) # alpine peaks at 5 threads for some reason, # all others scale past that (but try to avoid SMT), # 5 should be plenty anyways (3 GiB/s on most machines) hcores = min(CORES, 5 if CORES > 8 else 4) tty = os.environ.get("TERM", "").lower() == "linux" srvname = get_srvname(verbose) add_general(ap, nc, srvname) add_network(ap) add_tls(ap, cert_path) add_cert(ap, cert_path) add_auth(ap) add_chpw(ap) add_qr(ap, tty) add_zeroconf(ap) add_zc_mdns(ap) add_zc_ssdp(ap) add_fs(ap) add_share(ap) add_upload(ap) add_db_general(ap, hcores) add_db_metadata(ap) add_thumbnail(ap) add_transcoding(ap) add_rss(ap) add_sftp(ap) add_ftp(ap) add_webdav(ap) add_tftp(ap) add_smb(ap) add_opds(ap) add_safety(ap) add_salt(ap, fk_salt, dk_salt, ah_salt) add_optouts(ap) add_shutdown(ap) add_yolo(ap) add_handlers(ap) add_hooks(ap) add_stats(ap) add_txt(ap) add_tail(ap) add_og(ap) add_ui(ap, retry) add_admin(ap) add_logging(ap) add_debug(ap) ap2 = ap.add_argument_group("help sections") sects = get_sects() for k, h, _ in sects: ap2.add_argument("--help-" + k, action="store_true", help=h) if retry: a = ["ascii", "replace"] for x in ap._actions: try: x.default = x.default.encode(*a).decode(*a) except: pass try: if x.help and x.help is not argparse.SUPPRESS: x.help = x.help.replace("└─", "`-").encode(*a).decode(*a) if retry > 2: x.help = RE_ANSI.sub("", x.help) except: pass ret = ap.parse_args(args=argv[1:]) for k, h, t in sects: k2 = "help_" + k.replace("-", "_") if vars(ret)[k2]: lprint("# %s help page (%s)" % (k, h)) lprint(t.rstrip() + "\033[0m") sys.exit(0) return ret def main(argv: Optional[list[str]] = None) -> None: if argv is None: argv = sys.argv if "--versionb" in argv: print(S_VERSION) sys.exit(0) time.strptime("19970815", "%Y%m%d") # python#7980 if WINDOWS: os.system("rem") # enables colors init_E(E) f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite {} | jinja {} | pyftpd {} | tftp {} | miko {}\n\033[0m' f = f.format( S_VERSION, CODENAME, S_BUILD_DT, PY_DESC.replace("[", "\033[90m["), SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER, MIKO_VER, ) lprint(f) if "--version" in argv: sys.exit(0) if "--license" in argv: showlic() sys.exit(0) if "--mimes" in argv: print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items()))) sys.exit(0) if EXE: print("pybin: {}\n".format(pybin), end="") for n, zs in enumerate(argv): if zs.startswith("--sfx-tpoke="): Daemon(sfx_tpoke, "sfx-tpoke", (zs.split("=", 1)[1],)) argv.pop(n) break ensure_locale() ensure_webdeps() argv = expand_cfg(argv) deprecated: list[tuple[str, str]] = [ ("--salt", "--warksalt"), ("--hdr-au-usr", "--idp-h-usr"), ("--idp-h-sep", "--idp-gsep"), ("--th-no-crop", "--th-crop=n"), ("--never-symlink", "--hardlink-only"), ] for dk, nk in deprecated: idx = -1 ov = "" for n, k in enumerate(argv): if k == dk or k.startswith(dk + "="): idx = n if "=" in k: ov = "=" + k.split("=", 1)[1] if idx < 0: continue msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m" lprint(msg.format(dk, nk)) argv[idx] = nk + ov time.sleep(2) da = len(argv) == 1 and not CFG_DEF try: if da: argv.extend(["--qr"]) if ANYWIN or not os.geteuid(): # win10 allows symlinks if admin; can be unexpected argv.extend(["-p80,443,3923", "--ign-ebind"]) except: pass if da: t = "no arguments provided; will use {}\n" lprint(t.format(" ".join(argv[1:]))) nc = 1024 try: import resource _, hard = resource.getrlimit(resource.RLIMIT_NOFILE) if hard > 0: # -1 == infinite nc = min(nc, int(hard / 4)) except: nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows retry = 0 for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]: try: al = run_argparse(argv, fmtr, retry, nc) dal = run_argparse([], fmtr, retry, nc, False) break except SystemExit: raise except: retry += 1 t = "WARNING: due to limitations in your terminal and/or OS, the helptext cannot be displayed correctly. Will show a simplified version due to the following error:\n[ %s ]:\n%s\n" lprint(t % (fmtr, min_ex())) try: assert al # type: ignore assert dal # type: ignore al.E = E # __init__ is not shared when oxidized except: sys.exit(1) quotecheck(al) if al.chdir: os.chdir(al.chdir) if al.ansi: al.no_ansi = False elif not al.no_ansi: al.ansi = VT100 if WINDOWS and not al.keep_qem and not al.ah_cli: try: disable_quickedit() except: lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n") if not al.ansi: al.wintitle = "" # propagate implications for k1, k2 in IMPLICATIONS: if getattr(al, k1, None): setattr(al, k2, True) # propagate unplications for k1, k2 in UNPLICATIONS: if getattr(al, k1): setattr(al, k2, False) protos = "ftp tftp sftp smb".split() opts = ["%s_i" % (zs,) for zs in protos] for opt in opts: if getattr(al, opt) == "-i": setattr(al, opt, al.i) for opt in ["i"] + opts: zs = getattr(al, opt) if not HAVE_IPV6 and zs == "::": zs = "0.0.0.0" setattr(al, opt, [x.strip() for x in zs.split(",")]) try: if "-" in al.p: lo, hi = [int(x) for x in al.p.split("-")] al.p = list(range(lo, hi + 1)) else: al.p = [int(x) for x in al.p.split(",")] except: raise Exception("invalid value for -p") for arg, kname, okays in [["--u2sort", "u2sort", "s n fs fn"]]: val = unicode(getattr(al, kname)) if val not in okays.split(): zs = "argument {} cannot be '{}'; try one of these: {}" raise Exception(zs.format(arg, val, okays)) if not al.qrs and [k for k in argv if k.startswith("--qr")]: al.qr = True if al.ihead: al.ihead = [x.lower() for x in al.ihead] if al.ohead: al.ohead = [x.lower() for x in al.ohead] if HAVE_SSL: if al.ssl_ver: configure_ssl_ver(al) if al.ciphers: configure_ssl_ciphers(al) else: warn("ssl module does not exist; cannot enable https") al.http_only = True if PY2 and WINDOWS and al.e2d: warn( "windows py2 cannot do unicode filenames with -e2d\n" + " (if you crash with codec errors then that is why)" ) if PY2 and al.smb: print("error: python2 cannot --smb") return if not PY36: al.no_scandir = True if not hasattr(os, "sendfile"): al.no_sendfile = True if not hasattr(select, "poll"): al.no_poll = True # signal.signal(signal.SIGINT, sighandler) SvcHub(al, dal, argv, "".join(printed)).run() if __name__ == "__main__": main() ================================================ FILE: copyparty/__version__.py ================================================ # coding: utf-8 VERSION = (1, 20, 12) CODENAME = "sftp is fine too" BUILD_DT = (2026, 3, 11) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) __version__ = S_VERSION __build_dt__ = S_BUILD_DT # I'm all ears ================================================ FILE: copyparty/authsrv.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import base64 import hashlib import json import os import re import stat import sys import threading import time from datetime import datetime from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E from .bos import bos from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap from .pwhash import PWHash from .util import ( DEF_MTE, DEF_MTH, EXTS, FAVICON_MIMES, FN_EMB, HAVE_SQLITE3, IMPLICATIONS, META_NOBOTS, MIMES, SQLITE_VER, UNPLICATIONS, UTC, ODict, Pebkac, absreal, afsenc, get_df, humansize, json_hesc, min_ex, odfusion, read_utf8, relchk, statdir, ub64enc, uncyg, undot, unhumanize, vjoin, vsplit, ) if HAVE_SQLITE3: import sqlite3 if True: # pylint: disable=using-constant-test from collections.abc import Iterable from typing import Any, Generator, Optional, Sequence, Union from .util import NamedLogger, RootLogger if TYPE_CHECKING: from .broker_mp import BrokerMp from .broker_thr import BrokerThr from .broker_util import BrokerCli # Vflags: TypeAlias = dict[str, str | bool | float | list[str]] # Vflags: TypeAlias = dict[str, Any] # Mflags: TypeAlias = dict[str, Vflags] if PY2: range = xrange # type: ignore 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 UP_MTE_MAP = { # db-order "w": "substr(w,1,16)", "up_ip": "ip", ".up_at": "at", "up_by": "un", } SEE_LOG = "see log for details" SEESLOG = " (see fileserver log for details)" SSEELOG = " ({})".format(SEE_LOG) BAD_CFG = "invalid config; {}".format(SEE_LOG) SBADCFG = " ({})".format(BAD_CFG) PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}") PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}") PTN_U_ANY = re.compile(r"(\${[u][}%])") PTN_G_ANY = re.compile(r"(\${[g][}%])") PTN_SIGIL = re.compile(r"(\${[ug][}%])") class CfgEx(Exception): pass class AXS(object): def __init__( self, uread: Optional[Union[list[str], set[str]]] = None, uwrite: Optional[Union[list[str], set[str]]] = None, umove: Optional[Union[list[str], set[str]]] = None, udel: Optional[Union[list[str], set[str]]] = None, uget: Optional[Union[list[str], set[str]]] = None, upget: Optional[Union[list[str], set[str]]] = None, uhtml: Optional[Union[list[str], set[str]]] = None, uadmin: Optional[Union[list[str], set[str]]] = None, udot: Optional[Union[list[str], set[str]]] = None, ) -> None: self.uread: set[str] = set(uread or []) self.uwrite: set[str] = set(uwrite or []) self.umove: set[str] = set(umove or []) self.udel: set[str] = set(udel or []) self.uget: set[str] = set(uget or []) self.upget: set[str] = set(upget or []) self.uhtml: set[str] = set(uhtml or []) self.uadmin: set[str] = set(uadmin or []) self.udot: set[str] = set(udot or []) def __repr__(self) -> str: ks = "uread uwrite umove udel uget upget uhtml uadmin udot".split() return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),) class Lim(object): def __init__( self, args: argparse.Namespace, log_func: Optional["RootLogger"] ) -> None: self.log_func = log_func self.use_scandir = not args.no_scandir self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry self.chmod_d = 0o755 self.uid = self.gid = -1 self.chown = False self.nups: dict[str, list[float]] = {} # num tracker self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list self.bupc: dict[str, int] = {} # byte tracker cache self.nosub = False # disallow subdirectories self.dfl = 0 # free disk space limit self.dft = 0 # last-measured time self.dfv = 0 # currently free self.vbmax = 0 # volume bytes max self.vnmax = 0 # volume max num files self.c_vb_v = 0 # cache: volume bytes used (value) self.c_vb_r = 0 # cache: volume bytes used (ref) self.smin = 0 # filesize min self.smax = 0 # filesize max self.bwin = 0 # bytes window self.bmax = 0 # bytes max self.nwin = 0 # num window self.nmax = 0 # num max self.rotn = 0 # rot num files self.rotl = 0 # rot depth self.rotf = "" # rot datefmt self.rotf_tz = UTC # rot timezone self.rot_re = re.compile("") # rotf check def log(self, msg: str, c: Union[int, str] = 0) -> None: if self.log_func: self.log_func("up-lim", msg, c) def set_rotf(self, fmt: str, tz: str) -> None: self.rotf = fmt.rstrip("/\\") if tz != "UTC": from zoneinfo import ZoneInfo self.rotf_tz = ZoneInfo(tz) r = re.escape(self.rotf).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}") r = re.sub("%[mdHMSWU]", "[0-9]{2}", r) self.rot_re = re.compile("(^|/)" + r + "$") def all( self, ip: str, rem: str, sz: int, ptop: str, abspath: str, broker: Optional[Union["BrokerCli", "BrokerMp", "BrokerThr"]] = None, reg: Optional[dict[str, dict[str, Any]]] = None, volgetter: str = "up2k.get_volsize", ) -> tuple[str, str]: if reg is not None and self.reg is None: self.reg = reg self.dft = 0 self.chk_nup(ip) self.chk_bup(ip) self.chk_rem(rem) if sz != -1: self.chk_sz(sz) else: sz = 0 self.chk_vsz(broker, ptop, sz, volgetter) self.chk_df(abspath, sz) # side effects; keep last-ish ap2, vp2 = self.rot(abspath) if abspath == ap2: return ap2, rem return ap2, ("{}/{}".format(rem, vp2) if rem else vp2) def chk_sz(self, sz: int) -> None: if sz < self.smin: raise Pebkac(400, "file too small") if self.smax and sz > self.smax: raise Pebkac(400, "file too big") def chk_vsz( self, broker: Optional[Union["BrokerCli", "BrokerMp", "BrokerThr"]], ptop: str, sz: int, volgetter: str = "up2k.get_volsize", ) -> None: if not broker or not self.vbmax + self.vnmax: return x = broker.ask(volgetter, ptop) self.c_vb_v, nfiles = x.get() if self.vbmax and self.vbmax < self.c_vb_v + sz: raise Pebkac(400, "volume has exceeded max size") if self.vnmax and self.vnmax < nfiles + 1: raise Pebkac(400, "volume has exceeded max num.files") def chk_df(self, abspath: str, sz: int, already_written: bool = False) -> None: if not self.dfl: return if self.dft < time.time(): self.dft = int(time.time()) + 300 df, du, err = get_df(abspath, True) if err: t = "failed to read disk space usage for %r: %s" self.log(t % (abspath, err), 3) self.dfv = 0xAAAAAAAAA # 42.6 GiB else: self.dfv = df or 0 for j in list(self.reg.values()) if self.reg else []: self.dfv -= int(j["size"] / (len(j["hash"]) or 999) * len(j["need"])) if already_written: sz = 0 if self.dfv - sz < self.dfl: self.dft = min(self.dft, int(time.time()) + 10) t = "server HDD is full; {} free, need {}" raise Pebkac(500, t.format(humansize(self.dfv - self.dfl), humansize(sz))) self.dfv -= int(sz) def chk_rem(self, rem: str) -> None: if self.nosub and rem: raise Pebkac(500, "no subdirectories allowed") def rot(self, path: str) -> tuple[str, str]: if not self.rotf and not self.rotn: return path, "" if self.rotf: path = path.rstrip("/\\") if self.rot_re.search(path.replace("\\", "/")): return path, "" suf = datetime.now(self.rotf_tz).strftime(self.rotf) if path: path += "/" return path + suf, suf ret = self.dive(path, self.rotl) if not ret: raise Pebkac(500, "no available slots in volume") d = ret[len(path) :].strip("/\\").replace("\\", "/") return ret, d def dive(self, path: str, lvs: int) -> Optional[str]: if not lvs: # at leaf level items = statdir(self.log_func, self.use_scandir, False, path, True) items = [ x for x in items if not stat.S_ISDIR(x[1].st_mode) and not x[0].endswith(".PARTIAL") ] return None if len(items) >= self.rotn else "" items = bos.listdir(path) dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)] dirs.sort() if not dirs: # 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])) ret = self.dive(sub, lvs - 1) if ret is not None: return os.path.join(sub, ret) if len(dirs) >= self.rotn: # full branch or root return None # 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") return os.path.join(sub, ret) def nup(self, ip: str) -> None: try: self.nups[ip].append(time.time()) except: self.nups[ip] = [time.time()] def bup(self, ip: str, nbytes: int) -> None: v = (time.time(), nbytes) try: self.bups[ip].append(v) self.bupc[ip] += nbytes except: self.bups[ip] = [v] self.bupc[ip] = nbytes def chk_nup(self, ip: str) -> None: if not self.nmax or ip not in self.nups: return nups = self.nups[ip] cutoff = time.time() - self.nwin while nups and nups[0] < cutoff: nups.pop(0) if len(nups) >= self.nmax: raise Pebkac(429, "too many uploads") def chk_bup(self, ip: str) -> None: if not self.bmax or ip not in self.bups: return bups = self.bups[ip] cutoff = time.time() - self.bwin mark = self.bupc[ip] while bups and bups[0][0] < cutoff: mark -= bups.pop(0)[1] self.bupc[ip] = mark if mark >= self.bmax: raise Pebkac(429, "upload size limit exceeded") class VFS(object): """single level in the virtual fs""" def __init__( self, log: Optional["RootLogger"], realpath: str, vpath: str, vpath0: str, axs: AXS, flags: dict[str, Any], ) -> None: self.log = log self.realpath = realpath # absolute path on host filesystem self.vpath = vpath # absolute path in the virtual filesystem self.vpath0 = vpath0 # original vpath (before idp expansion) self.axs = axs self.uaxs: dict[ str, tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool] ] = {} self.flags = flags # config options self.root = self self.dev = 0 # st_dev self.nodes: dict[str, VFS] = {} # child nodes self.histtab: dict[str, str] = {} # all realpath->histpath self.dbpaths: dict[str, str] = {} # all realpath->dbpath self.dbv: Optional[VFS] = None # closest full/non-jump parent self.lim: Optional[Lim] = None # upload limits; only set for dbv self.shr_src: Optional[tuple[VFS, str]] = None # source vfs+rem of a share self.shr_files: set[str] = set() # filenames to include from shr_src self.shr_owner: str = "" # uname self.shr_all_aps: list[tuple[str, list[VFS]]] = [] self.aread: dict[str, list[str]] = {} self.awrite: dict[str, list[str]] = {} self.amove: dict[str, list[str]] = {} self.adel: dict[str, list[str]] = {} self.aget: dict[str, list[str]] = {} self.apget: dict[str, list[str]] = {} self.ahtml: dict[str, list[str]] = {} self.aadmin: dict[str, list[str]] = {} self.adot: dict[str, list[str]] = {} self.js_ls = {} self.js_htm = "" self.all_vols: dict[str, VFS] = {} # flattened recursive self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares self.all_fvols: dict[str, VFS] = {} # volumes which are files if realpath: rp = realpath + ("" if realpath.endswith(os.sep) else os.sep) vp = vpath + ("/" if vpath else "") self.histpath = os.path.join(realpath, ".hist") # db / thumbcache self.dbpath = self.histpath self.all_vols[vpath] = self self.all_nodes[vpath] = self self.all_aps = [(rp, [self])] self.all_vps = [(vp, self)] self.canonical = self._canonical self.dcanonical = self._dcanonical else: self.histpath = self.dbpath = "" self.all_aps = [] self.all_vps = [] self.canonical = self._canonical_null self.dcanonical = self._dcanonical_null self.get_dbv = self._get_dbv self.ls = self._ls def __repr__(self) -> str: return "VFS(%s)" % ( ", ".join( "%s=%r" % (k, self.__dict__[k]) for k in "realpath vpath axs flags".split() ) ) def get_all_vols( self, vols: dict[str, "VFS"], nodes: dict[str, "VFS"], aps: list[tuple[str, list["VFS"]]], vps: list[tuple[str, "VFS"]], ) -> None: nodes[self.vpath] = self if self.realpath: vols[self.vpath] = self rp = self.realpath rp += "" if rp.endswith(os.sep) else os.sep vp = self.vpath + ("/" if self.vpath else "") hit = next((x[1] for x in aps if x[0] == rp), None) if hit: hit.append(self) else: aps.append((rp, [self])) vps.append((vp, self)) for v in self.nodes.values(): v.get_all_vols(vols, nodes, aps, vps) def add(self, src: str, dst: str, dst0: str) -> "VFS": """get existing, or add new path to the vfs""" assert src == "/" or not src.endswith("/") # nosec assert not dst.endswith("/") # nosec if "/" in dst: # requires breadth-first population (permissions trickle down) name, dst = dst.split("/", 1) name0, dst0 = dst0.split("/", 1) if name in self.nodes: # exists; do not manipulate permissions return self.nodes[name].add(src, dst, dst0) vn = VFS( self.log, os.path.join(self.realpath, name) if self.realpath else "", "{}/{}".format(self.vpath, name).lstrip("/"), "{}/{}".format(self.vpath0, name0).lstrip("/"), self.axs, self._copy_flags(name), ) vn.dbv = self.dbv or self self.nodes[name] = vn return vn.add(src, dst, dst0) if dst in self.nodes: # leaf exists; return as-is return self.nodes[dst] # leaf does not exist; create and keep permissions blank vp = "{}/{}".format(self.vpath, dst).lstrip("/") vp0 = "{}/{}".format(self.vpath0, dst0).lstrip("/") vn = VFS(self.log, src, vp, vp0, AXS(), {}) vn.dbv = self.dbv or self self.nodes[dst] = vn return vn def _copy_flags(self, name: str) -> dict[str, Any]: flags = {k: v for k, v in self.flags.items()} hist = flags.get("hist") if hist and hist != "-": zs = "{}/{}".format(hist.rstrip("/"), name) flags["hist"] = os.path.expandvars(os.path.expanduser(zs)) dbp = flags.get("dbpath") if dbp and dbp != "-": zs = "{}/{}".format(dbp.rstrip("/"), name) flags["dbpath"] = os.path.expandvars(os.path.expanduser(zs)) return flags def bubble_flags(self) -> None: if self.dbv: for k, v in self.dbv.flags.items(): if k not in ("hist", "dbpath"): self.flags[k] = v for n in self.nodes.values(): n.bubble_flags() def _find(self, vpath: str) -> tuple["VFS", str]: """return [vfs,remainder]""" if not vpath: return self, "" if "/" in vpath: name, rem = vpath.split("/", 1) else: name = vpath rem = "" if name in self.nodes: return self.nodes[name]._find(rem) return self, vpath def can_access( self, vpath: str, uname: str ) -> tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]: """can Read,Write,Move,Delete,Get,Upget,Html,Admin,Dot""" # NOTE: only used by get_perms, which is only used by hooks; the lowest of fruits if vpath: vn, _ = self._find(undot(vpath)) else: vn = self return vn.uaxs[uname] def get_perms(self, vpath: str, uname: str) -> str: zbl = self.can_access(vpath, uname) ret = "".join(ch for ch, ok in zip("rwmdgGha.", zbl) if ok) if "rwmd" in ret and "a." in ret: ret += "A" return ret def get( self, vpath: str, uname: str, will_read: bool, will_write: bool, will_move: bool = False, will_del: bool = False, will_get: bool = False, err: int = 403, ) -> tuple["VFS", str]: """returns [vfsnode,fs_remainder] if user has the requested permissions""" if relchk(vpath): if self.log: self.log("vfs", "invalid relpath %r @%s" % (vpath, uname)) raise Pebkac(422) cvpath = undot(vpath) vn, rem = self._find(cvpath) c: AXS = vn.axs for req, d, msg in [ (will_read, c.uread, "read"), (will_write, c.uwrite, "write"), (will_move, c.umove, "move"), (will_del, c.udel, "delete"), (will_get, c.uget, "get"), ]: if req and uname not in d and uname != LEELOO_DALLAS: if vpath != cvpath and vpath != "." and self.log: ap = vn.canonical(rem) t = "%s has no %s in %r => %r => %r" self.log("vfs", t % (uname, msg, vpath, cvpath, ap), 6) t = "you don't have %s-access in %r or below %r" raise Pebkac(err, t % (msg, "/" + cvpath, "/" + vn.vpath)) return vn, rem def _get_share_src(self, vrem: str) -> tuple["VFS", str]: src = self.shr_src if not src: return self._get_dbv(vrem) shv, srem = src return shv._get_dbv(vjoin(srem, vrem)) def _get_dbv(self, vrem: str) -> tuple["VFS", str]: dbv = self.dbv if not dbv: return self, vrem vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem) return dbv, vrem def casechk(self, rem: str, do_stat: bool) -> bool: ap = self.canonical(rem, False) if do_stat and not bos.path.exists(ap): return True # doesn't exist at all; good to go dp, fn = os.path.split(ap) if not fn: return True # filesystem root try: fns = os.listdir(dp) except: return True # maybe chmod 111; assume ok if fn in fns: return True hit = "" lfn = fn.lower() for zs in fns: if lfn == zs.lower(): hit = zs break if not hit: return True # NFC/NFD or something, can't be helped either way if self.log: t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r" self.log("vfs", t % (fn, hit)) return False def _canonical_null(self, rem: str, resolve: bool = True) -> str: return "" def _dcanonical_null(self, rem: str) -> str: return "" def _canonical(self, rem: str, resolve: bool = True) -> str: """returns the canonical path (fully-resolved absolute fs path)""" ap = self.realpath if rem: ap += "/" + rem return absreal(ap) if resolve else ap def _dcanonical(self, rem: str) -> str: """resolves until the final component (filename)""" ap = self.realpath if rem: ap += "/" + rem ad, fn = os.path.split(ap) return os.path.join(absreal(ad), fn) def _canonical_shr(self, rem: str, resolve: bool = True) -> str: """returns the canonical path (fully-resolved absolute fs path)""" ap = self.realpath if rem: ap += "/" + rem rap = "" if self.shr_files: assert self.shr_src # !rm if rem and rem not in self.shr_files: return "\n\n" if resolve: rap = absreal(ap) vn, rem = self.shr_src chk = absreal(os.path.join(vn.realpath, rem)) if chk != rap: # not the dir itself; assert file allowed ad, fn = os.path.split(rap) if chk != ad or fn not in self.shr_files: return "\n\n" return (rap or absreal(ap)) if resolve else ap def _dcanonical_shr(self, rem: str) -> str: """resolves until the final component (filename)""" ap = self.realpath if rem: ap += "/" + rem ad, fn = os.path.split(ap) ad = absreal(ad) if self.shr_files: assert self.shr_src # !rm vn, rem = self.shr_src chk = absreal(os.path.join(vn.realpath, rem)) if chk != absreal(ap): # not the dir itself; assert file allowed if ad != chk or fn not in self.shr_files: return "\n\n" return os.path.join(ad, fn) def _ls_nope( self, *a, **ka ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: raise Pebkac(500, "nope.avi") def _ls_shr( self, rem: str, uname: str, scandir: bool, permsets: list[list[bool]], lstat: bool = False, throw: bool = False, ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: """replaces _ls for certain shares (single-file, or file selection)""" vn, rem = self.shr_src # type: ignore abspath, real, _ = vn.ls(rem, "\n", scandir, permsets, lstat, throw) real = [x for x in real if os.path.basename(x[0]) in self.shr_files] return abspath, real, {} def _ls( self, rem: str, uname: str, scandir: bool, permsets: list[list[bool]], lstat: bool = False, throw: bool = False, ) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]: """return user-readable [fsdir,real,virt] items at vpath""" virt_vis = {} # nodes readable by user abspath = self.canonical(rem) if abspath: real = list(statdir(self.log, scandir, lstat, abspath, throw)) real.sort() else: real = [] if not rem: # no vfs nodes in the list of real inodes real = [x for x in real if x[0] not in self.nodes] dbv = self.dbv or self for name, vn2 in sorted(self.nodes.items()): if vn2.dbv == dbv and self.flags.get("dk"): virt_vis[name] = vn2 continue u_has = vn2.uaxs.get(uname) or [False] * 9 for pset in permsets: ok = True for req, zb in zip(pset, u_has): if req and not zb: ok = False break if ok: virt_vis[name] = vn2 break if ".hist" in abspath: p = abspath.replace("\\", "/") if WINDOWS else abspath if p.endswith("/.hist"): real = [x for x in real if not x[0].startswith("up2k.")] elif "/.hist/th/" in p: real = [x for x in real if not x[0].endswith("dir.txt")] return abspath, real, virt_vis def walk( self, rel: str, rem: str, seen: list[str], uname: str, permsets: list[list[bool]], wantdots: int, scandir: bool, lstat: bool, subvols: bool = True, ) -> Generator[ tuple[ "VFS", str, str, str, list[tuple[str, os.stat_result]], list[tuple[str, os.stat_result]], dict[str, "VFS"], ], None, None, ]: """ recursively yields from ./rem; rel is a unix-style user-defined vpath (not vfs-related) NOTE: don't invoke this function from a dbv; subvols are only descended into if rem is blank due to the _ls `if not rem:` which intention is to prevent unintended access to subvols """ fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat) dbv, vrem = self.get_dbv(rem) if ( seen and (not fsroot.startswith(seen[-1]) or fsroot == seen[-1]) and fsroot in seen ): if self.log: t = "bailing from symlink loop,\n prev: %r\n curr: %r\n from: %r / %r" self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3) return if "xdev" in self.flags or "xvol" in self.flags: rm1 = [] for le in vfs_ls: ap = absreal(os.path.join(fsroot, le[0])) vn2 = self.chk_ap(ap) if not vn2 or not vn2.get("", uname, True, False): rm1.append(le) _ = [vfs_ls.remove(x) for x in rm1] # type: ignore dots_ok = wantdots and (wantdots == 2 or uname in dbv.axs.udot) if not dots_ok: vfs_ls = [x for x in vfs_ls if "/." not in "/" + x[0]] seen = seen[:] + [fsroot] rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)] rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] # if lstat: ignore folder symlinks since copyparty will never make those # (and we definitely don't want to descend into them) rfiles.sort() rdirs.sort() yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt for rdir, _ in rdirs: if not dots_ok and rdir.startswith("."): continue wrel = (rel + "/" + rdir).lstrip("/") wrem = (rem + "/" + rdir).lstrip("/") for x in self.walk( wrel, wrem, seen, uname, permsets, wantdots, scandir, lstat, subvols ): yield x if not subvols: return for n, vfs in sorted(vfs_virt.items()): if not dots_ok and n.startswith("."): continue wrel = (rel + "/" + n).lstrip("/") for x in vfs.walk( wrel, "", seen, uname, permsets, wantdots, scandir, lstat ): yield x def zipgen( self, vpath: str, vrem: str, flt: set[str], uname: str, dirs: bool, dots: int, scandir: bool, wrap: bool = True, ) -> Generator[dict[str, Any], None, None]: # if multiselect: add all items to archive root # if single folder: the folder itself is the top-level item folder = "" if flt or not wrap else (vpath.split("/")[-1].lstrip(".") or "top") g = self.walk(folder, vrem, [], uname, [[True, False]], dots, scandir, False) for _, _, vpath, apath, files, rd, vd in g: if flt: files = [x for x in files if x[0] in flt] rm1 = [x for x in rd if x[0] not in flt] _ = [rd.remove(x) for x in rm1] # type: ignore rm2 = [x for x in vd.keys() if x not in flt] _ = [vd.pop(x) for x in rm2] flt = set() # print(repr([vpath, apath, [x[0] for x in files]])) fnames = [n[0] for n in files] vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames apaths = [os.path.join(apath, n) for n in fnames] ret = list(zip(vpaths, apaths, files)) for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in ret]: yield f if not dirs: continue ts = int(time.time()) st = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, ts, ts, ts)) dnames = [n[0] for n in rd] dstats = [n[1] for n in rd] dnames += list(vd.keys()) dstats += [st] * len(vd) vpaths = [vpath + "/" + n for n in dnames] if vpath else dnames apaths = [os.path.join(apath, n) for n in dnames] ret2 = list(zip(vpaths, apaths, dstats)) for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]: yield d def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]: aps = ap + os.sep if "xdev" in self.flags and not ANYWIN: if not st: ap2 = ap.replace("\\", "/") if ANYWIN else ap while ap2: try: st = bos.stat(ap2) break except: if "/" not in ap2: raise ap2 = ap2.rsplit("/", 1)[0] assert st vdev = self.dev if not vdev: vdev = self.dev = bos.stat(self.realpath).st_dev if vdev != st.st_dev: if self.log: t = "xdev: %s[%r] => %s[%r]" self.log("vfs", t % (vdev, self.realpath, st.st_dev, ap), 3) return None if "xvol" in self.flags: self_ap = self.realpath + os.sep if aps.startswith(self_ap): vp = aps[len(self_ap) :] if ANYWIN: vp = vp.replace(os.sep, "/") vn2, _ = self._find(vp) if self == vn2: return self all_aps = self.shr_all_aps or self.root.all_aps for vap, vns in all_aps: if aps.startswith(vap): return self if self in vns else vns[0] if self.log: self.log("vfs", "xvol: %r" % (ap,), 3) return None return self def check_landmarks(self) -> bool: if self.dbv: return True vps = self.flags.get("landmark") or [] if not vps: return True failed = "" for vp in vps: if "^=" in vp: vp, zs = vp.split("^=", 1) expect = zs.encode("utf-8") else: expect = b"" if self.log: t = "checking [/%s] landmark [%s]" self.log("vfs", t % (self.vpath, vp), 6) ap = "?" try: ap = self.canonical(vp) with open(ap, "rb") as f: buf = f.read(4096) if not buf.startswith(expect): t = "file [%s] does not start with the expected bytes %s" failed = t % (ap, expect) break except Exception as ex: t = "%r while trying to read [%s] => [%s]" failed = t % (ex, vp, ap) break if not failed: return True if self.log: t = "WARNING: landmark verification failed; %s; will now disable up2k database for volume [/%s]" self.log("vfs", t % (failed, self.vpath), 3) for rm in "e2d e2t e2v".split(): self.flags = {k: v for k, v in self.flags.items() if not k.startswith(rm)} self.flags["d2d"] = True self.flags["d2t"] = True return False if WINDOWS: re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$") else: re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$") class AuthSrv(object): """verifies users against given paths""" def __init__( self, args: argparse.Namespace, log_func: Optional["RootLogger"], warn_anonwrite: bool = True, dargs: Optional[argparse.Namespace] = None, ) -> None: self.ah = PWHash(args) self.args = args self.dargs = dargs or args self.log_func = log_func self.warn_anonwrite = warn_anonwrite self.line_ctr = 0 self.indent = "" self.is_lxc = args.c == ["/z/initcfg"] oh = "X-Content-Type-Options: nosniff\r\n" if self.args.http_vary: oh += "Vary: %s\r\n" % (self.args.http_vary,) self._vf0b = { "oh_g": oh + "\r\n", "oh_f": oh + "\r\n", "cachectl": self.args.cachectl, "tcolor": self.args.tcolor, "du_iwho": self.args.du_iwho, "shr_who": self.args.shr_who if self.args.shr else "no", "emb_all": FN_EMB, "ls_q_m": ("", ""), } self._vf0 = self._vf0b.copy() self._vf0["d2d"] = True # fwd-decl self.vfs = VFS(log_func, "", "", "", AXS(), {}) self.acct: dict[str, str] = {} # uname->pw self.iacct: dict[str, str] = {} # pw->uname self.ases: dict[str, str] = {} # uname->session self.sesa: dict[str, str] = {} # session->uname self.defpw: dict[str, str] = {} self.grps: dict[str, list[str]] = {} self.re_pwd: Optional[re.Pattern] = None self.cfg_files_loaded: list[str] = [] self.badcfg1 = False # all volumes observed since last restart self.idp_vols: dict[str, str] = {} # vpath->abspath # all users/groups observed since last restart self.idp_accs: dict[str, list[str]] = {} # username->groupnames self.idp_usr_gh: dict[str, str] = {} # username->group-header-value (cache) self.hid_cache: dict[str, str] = {} self.mutex = threading.Lock() self.reload() def log(self, msg: str, c: Union[int, str] = 0) -> None: if self.log_func: self.log_func("auth", msg, c) def laggy_iter(self, iterable: Iterable[Any]) -> Generator[Any, None, None]: """returns [value,isFinalValue]""" it = iter(iterable) prev = next(it) for x in it: yield prev, False prev = x yield prev, True def vf0(self): return self._vf0.copy() def vf0b(self): return self._vf0b.copy() def idp_checkin( self, broker: Optional["BrokerCli"], uname: str, gname: str ) -> bool: if self.idp_usr_gh.get(uname) == gname: return False gnames = [x.strip() for x in self.args.idp_gsep.split(gname)] gnames.sort() with self.mutex: self.idp_usr_gh[uname] = gname if self.idp_accs.get(uname) == gnames: return False self.idp_accs[uname] = gnames try: self._update_idp_db(uname, gname) except: self.log("failed to update the --idp-db:\n%s" % (min_ex(),), 3) t = "reinitializing due to new user from IdP: [%r:%r]" self.log(t % (uname, gnames), 3) if not broker: # only true for tests self._reload() return True broker.ask("reload", False, True).get() return True def _update_idp_db(self, uname: str, gname: str) -> None: if not self.args.idp_store: return assert sqlite3 # type: ignore # !rm db = sqlite3.connect(self.args.idp_db) cur = db.cursor() cur.execute("delete from us where un = ?", (uname,)) cur.execute("insert into us values (?,?)", (uname, gname)) db.commit() cur.close() db.close() def _map_volume_idp( self, src: str, dst: str, mount: dict[str, tuple[str, str]], daxs: dict[str, AXS], mflags: dict[str, dict[str, Any]], un_gns: dict[str, list[str]], ) -> list[tuple[str, str, str, str]]: ret: list[tuple[str, str, str, str]] = [] visited = set() src0 = src # abspath dst0 = dst # vpath zsl = [] for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")): if bool(ptn.search(src)) != bool(ptn.search(dst)): zsl.append(sigil) if zsl: t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]" t = "\n".join([t % (x, src, dst) for x in zsl]) self.log(t, 1) raise Exception(t) un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns] if not un_gn: # ensure volume creation if there's no users un_gn = [("", "")] for un, gn in un_gn: rejected = False for ptn in [PTN_U_GRP, PTN_G_GRP]: m = ptn.search(dst0) if not m: continue zs = m.group(1) zs = zs.replace(",%+", "\n%+") zs = zs.replace(",%-", "\n%-") for rule in zs.split("\n"): gnc = rule[2:] if ptn == PTN_U_GRP: # is user member of group? hit = gnc in (un_gns.get(un) or []) else: # is it this specific group? hit = gn == gnc if rule.startswith("%+") != hit: rejected = True if rejected: continue if gn == self.args.grp_all: gn = "" # if ap/vp has a user/group placeholder, make sure to keep # track so the same user/group is mapped when setting perms; # otherwise clear un/gn to indicate it's a regular volume src1 = src0.replace("${u}", un or "\n") dst1 = dst0.replace("${u}", un or "\n") src1 = PTN_U_GRP.sub(un or "\n", src1) dst1 = PTN_U_GRP.sub(un or "\n", dst1) if src0 == src1 and dst0 == dst1: un = "" src = src1.replace("${g}", gn or "\n") dst = dst1.replace("${g}", gn or "\n") src = PTN_G_GRP.sub(gn or "\n", src) dst = PTN_G_GRP.sub(gn or "\n", dst) if src == src1 and dst == dst1: gn = "" if "\n" in (src + dst): continue label = "%s\n%s" % (src, dst) if label in visited: continue visited.add(label) src, dst = self._map_volume(src, dst, dst0, mount, daxs, mflags) if src: ret.append((src, dst, un, gn)) if un or gn: self.idp_vols[dst] = src return ret def _map_volume( self, src: str, dst: str, dst0: str, mount: dict[str, tuple[str, str]], daxs: dict[str, AXS], mflags: dict[str, dict[str, Any]], ) -> tuple[str, str]: src = os.path.expandvars(os.path.expanduser(src)) src = absreal(src) dst = dst.strip("/") if dst in mount: t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]" self.log(t.format(dst, mount[dst][0], src), c=1) raise Exception(BAD_CFG) if src in mount.values(): t = "filesystem-path [{}] mounted in multiple locations:" t = t.format(src) for v in [k for k, v in mount.items() if v[0] == src] + [dst]: t += "\n /{}".format(v) self.log(t, c=3) raise Exception(BAD_CFG) if not bos.path.exists(src): self.log("warning: filesystem-path did not exist: %r" % (src,), 3) vf = {} if dst.startswith(".") or "/." in dst: vf["unlistcr"] = True vf["unlistcw"] = True mount[dst] = (src, dst0) daxs[dst] = AXS() mflags[dst] = vf return (src, dst) def _e(self, desc: Optional[str] = None) -> None: if not self.args.vc or not self.line_ctr: return if not desc and not self.indent: self.log("") return desc = desc or "" desc = desc.replace("[", "[\033[0m").replace("]", "\033[90m]") self.log(" >>> {}{}".format(self.indent, desc), "90") def _l(self, ln: str, c: int, desc: str) -> None: if not self.args.vc or not self.line_ctr: return if c < 10: c += 30 t = "\033[97m{:4} \033[{}m{}{}" if desc: t += " \033[0;90m# {}\033[0m" desc = desc.replace("[", "[\033[0m").replace("]", "\033[90m]") self.log(t.format(self.line_ctr, c, self.indent, ln, desc)) def _all_un_gn( self, acct: dict[str, str], grps: dict[str, list[str]], ) -> dict[str, list[str]]: """ generate list of all confirmed pairs of username/groupname seen since last restart; in case of conflicting group memberships then it is selected as follows: * any non-zero value from IdP group header * otherwise take --grps / [groups] """ self.load_idp_db(bool(self.idp_accs)) ret = {un: gns[:] for un, gns in self.idp_accs.items()} ret.update({zs: [""] for zs in acct if zs not in ret}) grps[self.args.grp_all] = list(ret.keys()) for gn, uns in grps.items(): for un in uns: try: ret[un].append(gn) except: ret[un] = [gn] return ret def _parse_config_file( self, fp: str, cfg_lines: list[str], acct: dict[str, str], grps: dict[str, list[str]], daxs: dict[str, AXS], mflags: dict[str, dict[str, Any]], mount: dict[str, tuple[str, str]], ) -> None: self.line_ctr = 0 expand_config_file(self.log, cfg_lines, fp, "") if self.args.vc: lns = ["{:4}: {}".format(n, s) for n, s in enumerate(cfg_lines, 1)] self.log("expanded config file (unprocessed):\n" + "\n".join(lns)) cfg_lines = upgrade_cfg_fmt(self.log, self.args, cfg_lines, fp) # due to IdP, volumes must be parsed after users and groups; # do volumes in a 2nd pass to allow arbitrary order in config files for npass in range(1, 3): if self.args.vc: self.log("parsing config files; pass %d/%d" % (npass, 2)) self._parse_config_file_2(cfg_lines, acct, grps, daxs, mflags, mount, npass) def _parse_config_file_2( self, cfg_lines: list[str], acct: dict[str, str], grps: dict[str, list[str]], daxs: dict[str, AXS], mflags: dict[str, dict[str, Any]], mount: dict[str, tuple[str, str]], npass: int, ) -> None: self.line_ctr = 0 all_un_gn = self._all_un_gn(acct, grps) cat = "" catg = "[global]" cata = "[accounts]" catgrp = "[groups]" catx = "accs:" catf = "flags:" ap: Optional[str] = None vp: Optional[str] = None vols: list[tuple[str, str, str, str]] = [] for ln in cfg_lines: self.line_ctr += 1 ln = ln.split(" #")[0].strip() if not ln.split("#")[0].strip(): continue if re.match(r"^\[.*\]:$", ln): ln = ln[:-1] subsection = ln in (catx, catf) if ln.startswith("[") or subsection: self._e() if npass > 1 and ap is None and vp is not None: t = "the first line after [/{}] must be a filesystem path to share on that volume" raise Exception(t.format(vp)) cat = ln if not subsection: ap = vp = None self.indent = "" else: self.indent = " " if ln == catg: t = "begin commandline-arguments (anything from --help; dashes are optional)" self._l(ln, 6, t) elif ln == cata: self._l(ln, 5, "begin user-accounts section") elif ln == catgrp: self._l(ln, 5, "begin user-groups section") elif ln.startswith("[/"): vp = ln[1:-1].strip("/") self._l(ln, 2, "define volume at URL [/{}]".format(vp)) elif subsection: if ln == catx: self._l(ln, 5, "volume access config:") else: t = "volume-specific config (anything from --help-flags)" self._l(ln, 6, t) else: raise Exception("invalid section header" + SBADCFG) self.indent = " " if subsection else " " continue if cat == catg: self._l(ln, 6, "") zt = split_cfg_ln(ln) for zs, za in zt.items(): zs = zs.lstrip("-") if "=" in zs: t = "WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?" zs1, zs2 = zs.split("=", 1) self.log(t % (zs, zs1, zs2), 1) if za is True: self._e("└─argument [{}]".format(zs)) else: self._e("└─argument [{}] with value [{}]".format(zs, za)) continue if cat == cata: try: u, p = [zs.strip() for zs in ln.split(":", 1)] if "=" in u and not p: t = "WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?" zs1, zs2 = u.split("=", 1) self.log(t % (u, zs1, zs2), 3) self._l(ln, 5, "account [{}], password [{}]".format(u, p)) acct[u] = p except: t = 'lines inside the [accounts] section must be "username: password"' raise Exception(t + SBADCFG) continue if cat == catgrp: try: gn, zs1 = [zs.strip() for zs in ln.split(":", 1)] uns = [zs.strip() for zs in zs1.split(",")] t = "group [%s] = " % (gn,) t += ", ".join("user [%s]" % (x,) for x in uns) self._l(ln, 5, t) if gn in grps: grps[gn].extend(uns) else: grps[gn] = uns except: t = 'lines inside the [groups] section must be "groupname: user1, user2, user..."' raise Exception(t + SBADCFG) continue if vp is not None and ap is None: if npass != 2: continue ap = ln self._l(ln, 2, "bound to filesystem-path [{}]".format(ap)) vols = self._map_volume_idp(ap, vp, mount, daxs, mflags, all_un_gn) if not vols: ap = vp = None self._l(ln, 2, "└─no users/groups known; was not mapped") elif len(vols) > 1: for vol in vols: self._l(ln, 2, "└─mapping: [%s] => [%s]" % (vol[1], vol[0])) continue if cat == catx: if npass != 2 or not ap: # not stage2, or unmapped ${u}/${g} continue err = "" try: self._l(ln, 5, "volume access config:") sk, sv = ln.split(":") if re.sub("[rwmdgGhaA.]", "", sk) or not sk: err = "invalid accs permissions list; " raise Exception(err) if " " in re.sub(", *", "", sv).strip(): err = "list of users is not comma-separated; " raise Exception(err) sv = sv.replace(" ", "") self._read_vol_str_idp(sk, sv, vols, all_un_gn, daxs, mflags) continue except CfgEx: raise except: err += "accs entries must be 'rwmdgGhaA.: user1, user2, ...'" raise CfgEx(err + SBADCFG) if cat == catf: if npass != 2 or not ap: # not stage2, or unmapped ${u}/${g} continue err = "" try: self._l(ln, 6, "volume-specific config:") zd = split_cfg_ln(ln) fstr = "" for sk, sv in zd.items(): if "=" in sk: t = "WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?" zs1, zs2 = sk.split("=", 1) self.log(t % (sk, zs1, zs2), 3) bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-") if bad: err = "bad characters [{}] in volflag name [{}]; " err = err.format(bad, sk) raise Exception(err + SBADCFG) if sv is True: fstr += "," + sk else: fstr += ",{}={}".format(sk, sv) assert vp is not None self._read_vol_str_idp( "c", fstr[1:], vols, all_un_gn, daxs, mflags ) fstr = "" if fstr: self._read_vol_str_idp( "c", fstr[1:], vols, all_un_gn, daxs, mflags ) continue except: err += "flags entries (volflags) must be one of the following:\n 'flag1, flag2, ...'\n 'key: value'\n 'flag1, flag2, key: value'" raise Exception(err + SBADCFG) raise Exception("unprocessable line in config" + SBADCFG) self._e() self.line_ctr = 0 def _read_vol_str_idp( self, lvl: str, uname: str, vols: list[tuple[str, str, str, str]], un_gns: dict[str, list[str]], axs: dict[str, AXS], flags: dict[str, dict[str, Any]], ) -> None: if lvl.strip("crwmdgGhaA."): t = "%s,%s" % (lvl, uname) if uname else lvl raise CfgEx("invalid config value (volume or volflag): %s" % (t,)) if lvl == "c": # here, 'uname' is not a username; it is a volflag name... sorry cval: Union[bool, str] = True try: # volflag with arguments, possibly with a preceding list of bools uname, cval = uname.split("=", 1) except: # just one or more bools pass while "," in uname: # one or more bools before the final flag; eat them n1, uname = uname.split(",", 1) for _, vp, _, _ in vols: self._read_volflag(vp, flags[vp], n1, True, False) for _, vp, _, _ in vols: self._read_volflag(vp, flags[vp], uname, cval, False) return if uname == "": uname = "*" unames = [] for un in uname.replace(",", " ").strip().split(): if un.startswith("@"): grp = un[1:] uns = [x[0] for x in un_gns.items() if grp in x[1]] if grp == "${g}": unames.append(un) elif not uns and not self.args.idp_h_grp and grp != self.args.grp_all: t = "group [%s] must be defined with --grp argument (or in a [groups] config section)" raise CfgEx(t % (grp,)) unames.extend(uns) else: unames.append(un) # unames may still contain ${u} and ${g} so now expand those; un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns] for src, dst, vu, vg in vols: unames2 = set(unames) if "${u}" in unames: if not vu: t = "cannot use ${u} in accs of volume [%s] because the volume url does not contain ${u}" raise CfgEx(t % (src,)) unames2.add(vu) if "@${g}" in unames: if not vg: t = "cannot use @${g} in accs of volume [%s] because the volume url does not contain @${g}" raise CfgEx(t % (src,)) unames2.update([un for un, gn in un_gn if gn == vg]) if "${g}" in unames: t = 'the accs of volume [%s] contains "${g}" but the only supported way of specifying that is "@${g}"' raise CfgEx(t % (src,)) unames2.discard("${u}") unames2.discard("@${g}") self._read_vol_str(lvl, list(unames2), axs[dst]) def _read_vol_str(self, lvl: str, unames: list[str], axs: AXS) -> None: junkset = set() for un in unames: for alias, mapping in [ ("h", "gh"), ("G", "gG"), ("A", "rwmda.A"), ]: expanded = "" for ch in mapping: if ch not in lvl: expanded += ch lvl = lvl.replace(alias, expanded + alias) for ch, al in [ ("r", axs.uread), ("w", axs.uwrite), ("m", axs.umove), ("d", axs.udel), (".", axs.udot), ("a", axs.uadmin), ("A", junkset), ("g", axs.uget), ("G", axs.upget), ("h", axs.uhtml), ]: if ch in lvl: if un == "*": t = "└─add permission [{0}] for [everyone] -- {2}" else: t = "└─add permission [{0}] for user [{1}] -- {2}" desc = permdescs.get(ch, "?") self._e(t.format(ch, un, desc)) al.add(un) def _read_volflag( self, vpath: str, flags: dict[str, Any], name: str, value: Union[str, bool, list[str]], is_list: bool, ) -> None: if name not in flagdescs: name = name.lower() # volflags are snake_case, but a leading dash is the removal operator stripped = name.lstrip("-") zi = len(name) - len(stripped) if zi > 1: t = "WARNING: the config for volume [/%s] specified a volflag with multiple leading hyphens (%s); use one hyphen to remove, or zero hyphens to add a flag. Will now enable flag [%s]" self.log(t % (vpath, name, stripped), 3) name = stripped zi = 0 if stripped not in flagdescs and "-" in stripped: name = ("-" * zi) + stripped.replace("-", "_") desc = flagdescs.get(name.lstrip("-"), "?").replace("\n", " ") if not name: self._e("└─unreadable-line") t = "WARNING: the config for volume [/%s] indicated that a volflag was to be defined, but the volflag name was blank" self.log(t % (vpath,), 3) return if re.match("^-[^-]+$", name): t = "└─unset volflag [{}] ({})" self._e(t.format(name[1:], desc)) flags[name] = True return zs = "ext_th landmark mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban" if name not in zs.split(): if value is True: t = "└─add volflag [{}] = {} ({})" else: t = "└─add volflag [{}] = [{}] ({})" self._e(t.format(name, value, desc)) flags[name] = value return vals = flags.get(name, []) if not value: return elif is_list: vals += value else: vals += [value] flags[name] = vals self._e("volflag [{}] += {} ({})".format(name, vals, desc)) def reload(self, verbosity: int = 9) -> None: """ construct a flat list of mountpoints and usernames first from the commandline arguments then supplementing with config files before finally building the VFS """ with self.mutex: self._reload(verbosity) def _reload(self, verbosity: int = 9) -> None: acct: dict[str, str] = {} # username:password grps: dict[str, list[str]] = {} # groupname:usernames daxs: dict[str, AXS] = {} mflags: dict[str, dict[str, Any]] = {} # vpath:flags mount: dict[str, tuple[str, str]] = {} # dst:src (vp:(ap,vp0)) cfg_files_loaded: list[str] = [] self.idp_vols = {} # yolo self.badcfg1 = False if self.args.a: # list of username:password for x in self.args.a: try: u, p = x.split(":", 1) acct[u] = p except: t = '\n invalid value "{}" for argument -a, must be username:password' raise Exception(t.format(x)) if self.args.grp: # list of groupname:username,username,... for x in self.args.grp: try: # accept both = and : as separator between groupname and usernames, # accept both , and : as separators between usernames zs1, zs2 = x.replace("=", ":").split(":", 1) grps[zs1] = zs2.replace(":", ",").split(",") grps[zs1] = [x.strip() for x in grps[zs1]] except: t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...' raise Exception(t.format(x)) if self.args.v: # list of src:dst:permset:permset:... # permset is [,username][,username] or ,[=args] all_un_gn = self._all_un_gn(acct, grps) for v_str in self.args.v: m = re_vol.match(v_str) if not m: raise Exception("invalid -v argument: [{}]".format(v_str)) src, dst, perms = m.groups() if WINDOWS: src = uncyg(src) vols = self._map_volume_idp(src, dst, mount, daxs, mflags, all_un_gn) for x in perms.split(":"): lvl, uname = x.split(",", 1) if "," in x else [x, ""] self._read_vol_str_idp(lvl, uname, vols, all_un_gn, daxs, mflags) if self.args.c: for cfg_fn in self.args.c: lns: list[str] = [] try: self._parse_config_file( cfg_fn, lns, acct, grps, daxs, mflags, mount ) zs = "#\033[36m cfg files in " zst = [x[len(zs) :] for x in lns if x.startswith(zs)] for zs in list(set(zst)): self.log("discovered config files in " + zs, 6) zs = "#\033[36m opening cfg file" zstt = [x.split(" -> ") for x in lns if x.startswith(zs)] zst = [(max(0, len(x) - 2) * " ") + "└" + x[-1] for x in zstt] t = "loaded {} config files:\n{}" self.log(t.format(len(zst), "\n".join(zst))) cfg_files_loaded = zst except: lns = lns[: self.line_ctr] slns = ["{:4}: {}".format(n, s) for n, s in enumerate(lns, 1)] t = "\033[1;31m\nerror @ line {}, included from {}\033[0m" t = t.format(self.line_ctr, cfg_fn) self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns))) raise derive_args(self.args) self.setup_auth_ord() if self.args.ipu and not self.args.have_idp_hdrs: # syntax (CIDR=UNAME) is verified in load_ipu zsl = [x.split("=", 1)[1] for x in self.args.ipu] zsl = [x for x in zsl if x and x not in acct] if zsl: t = "ERROR: unknown users in ipu: %s" % (zsl,) self.log(t, 1) raise Exception(t) self.setup_pwhash(acct) defpw = acct.copy() self.setup_chpw(acct) # case-insensitive; normalize if WINDOWS: cased = {} for vp, (ap, vp0) in mount.items(): cased[vp] = (absreal(ap), vp0) mount = cased if not mount and not self.args.have_idp_hdrs: # -h says our defaults are CWD at root and read/write for everyone axs = AXS(["*"], ["*"], None, None) ehint = "" if self.is_lxc: t = "Read-access has been disabled due to failsafe: Docker detected, but %s. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to all of /w/ by adding the following arguments to the docker container: -v .::rw" if len(cfg_files_loaded) == 1: self.log(t % ("no config-file was provided",), 1) t = "it is strongly recommended to add a config-file instead, for example based on https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/copyparty.conf" self.log(t, 3) else: self.log(t % ("the config does not define any volumes",), 1) axs = AXS() ehint = "; please try moving them up one level, into the parent folder:" elif self.args.c: t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw" self.log(t, 1) axs = AXS() ehint = ":" if ehint: try: files = os.listdir(E.cfg) except: files = [] hits = [ x for x in files if x.lower().endswith(".conf") and not x.startswith(".") ] if hits: t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n" self.log(t % (E.cfg, ehint, ", ".join(hits)), 3) vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b()) if not axs.uread: self.badcfg1 = True elif "" not in mount: # there's volumes but no root; make root inaccessible vfs = VFS(self.log_func, "", "", "", AXS(), self.vf0()) maxdepth = 0 for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))): depth = dst.count("/") assert maxdepth <= depth # nosec maxdepth = depth src, dst0 = mount[dst] if dst == "": # rootfs was mapped; fully replaces the default CWD vfs vfs = VFS(self.log_func, src, dst, dst0, daxs[dst], mflags[dst]) continue assert vfs # type: ignore zv = vfs.add(src, dst, dst0) zv.axs = daxs[dst] zv.flags = mflags[dst] zv.dbv = None assert vfs # type: ignore vfs.all_vols = {} vfs.all_nodes = {} vfs.all_aps = [] vfs.all_vps = [] vfs.get_all_vols(vfs.all_vols, vfs.all_nodes, vfs.all_aps, vfs.all_vps) for vol in vfs.all_nodes.values(): vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True) vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True) vol.root = vfs zs = "du_iwho emb_all ls_q_m neversymlink oh_f oh_g" k_ign = set(zs.split()) for vol in vfs.all_vols.values(): unknown_flags = set() for k, v in vol.flags.items(): ks = k.lstrip("-") if ks not in flagdescs and ks not in k_ign: unknown_flags.add(k) if unknown_flags: t = "WARNING: the config for volume [/%s] has unrecognized volflags; will ignore: '%s'" self.log(t % (vol.vpath, "', '".join(unknown_flags)), 3) enshare = self.args.shr shr = enshare[1:-1] shrs = enshare[1:] if enshare: assert sqlite3 # type: ignore # !rm shv = VFS(self.log_func, "", shr, shr, AXS(), self.vf0()) db_path = self.args.shr_db db = sqlite3.connect(db_path) cur = db.cursor() cur2 = db.cursor() now = time.time() for row in cur.execute("select * from sh"): s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row if s_t1 and s_t1 < now: continue if self.args.shr_v: t = "loading %s share %r by %r => %r" self.log(t % (s_pr, s_k, s_un, s_vp)) if s_pw: # gotta reuse the "account" for all shares with this pw, # so do a light scramble as this appears in the web-ui zb = hashlib.sha512(s_pw.encode("utf-8")).digest() sun = "s_%s" % (ub64enc(zb)[4:16].decode("ascii"),) acct[sun] = s_pw else: sun = "*" s_axs = AXS( [sun] if "r" in s_pr else [], [sun] if "w" in s_pr else [], [sun] if "m" in s_pr else [], [sun] if "d" in s_pr else [], [sun] if "g" in s_pr else [], ) # don't know the abspath yet + wanna ensure the user # still has the privs they granted, so nullmap it vp = "%s/%s" % (shr, s_k) shv.nodes[s_k] = VFS(self.log_func, "", vp, vp, s_axs, shv.flags.copy()) vfs.nodes[shr] = vfs.all_vols[shr] = shv for vol in shv.nodes.values(): vfs.all_vols[vol.vpath] = vfs.all_nodes[vol.vpath] = vol vol.get_dbv = vol._get_share_src vol.ls = vol._ls_nope zss = set(acct) zss.update(self.idp_accs) zss.discard("*") unames = ["*"] + list(sorted(zss)) for perm in "read write move del get pget html admin dot".split(): axs_key = "u" + perm for vp, vol in vfs.all_vols.items(): zx = getattr(vol.axs, axs_key) if "*" in zx and "-@acct" not in zx: for usr in unames: zx.add(usr) for zs in list(zx): if zs.startswith("-"): zx.discard(zs) zs = zs[1:] zx.discard(zs) if zs.startswith("@"): zs = zs[1:] for zs in grps.get(zs) or []: zx.discard(zs) # aread,... = dict[uname, list[volnames] or []] umap: dict[str, list[str]] = {x: [] for x in unames} for usr in unames: for vp, vol in vfs.all_vols.items(): zx = getattr(vol.axs, axs_key) if usr in zx and (not enshare or not vp.startswith(shrs)): umap[usr].append(vp) umap[usr].sort() setattr(vfs, "a" + perm, umap) for vol in vfs.all_nodes.values(): za = vol.axs vol.uaxs = { un: ( un in za.uread, un in za.uwrite, un in za.umove, un in za.udel, un in za.uget, un in za.upget, un in za.uhtml, un in za.uadmin, un in za.udot, ) for un in unames } all_users = {} missing_users = {} associated_users = {} for axs in daxs.values(): for d in [ axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget, axs.upget, axs.uhtml, axs.uadmin, axs.udot, ]: for usr in d: all_users[usr] = 1 if usr != "*" and usr not in acct and usr not in self.idp_accs: missing_users[usr] = 1 if "*" not in d: associated_users[usr] = 1 if missing_users: zs = ", ".join(k for k in sorted(missing_users)) if self.args.have_idp_hdrs: t = "the following users are unknown, and assumed to come from IdP: " self.log(t + zs, c=6) else: t = "you must -a the following users: " self.log(t + zs, c=1) raise Exception(BAD_CFG) if LEELOO_DALLAS in all_users: raise Exception("sorry, reserved username: " + LEELOO_DALLAS) zsl = [] for usr in list(acct)[:]: zs = acct[usr].strip() if not zs: zs = ub64enc(os.urandom(48)).decode("ascii") zsl.append(usr) acct[usr] = zs if zsl: self.log("generated random passwords for users %r" % (zsl,), 6) seenpwds = {} for usr, pwd in acct.items(): if pwd in seenpwds: t = "accounts [{}] and [{}] have the same password; this is not supported" self.log(t.format(seenpwds[pwd], usr), 1) raise Exception(BAD_CFG) seenpwds[pwd] = usr for usr in acct: if usr not in associated_users: if enshare and usr.startswith("s_"): continue if len(vfs.all_vols) > 1: # user probably familiar enough that the verbose message is not necessary t = "account [%s] is not mentioned in any volume definitions; see --help-accounts" self.log(t % (usr,), 1) else: t = "WARNING: the account [%s] is not mentioned in any volume definitions and thus has the same access-level and privileges that guests have; please see --help-accounts for details. For example, if you intended to give that user full access to the current directory, you could do this: -v .::A,%s" self.log(t % (usr, usr), 1) dropvols = [] errors = False for vol in vfs.all_vols.values(): if ( not vol.realpath or ( "assert_root" not in vol.flags and "nospawn" not in vol.flags and not self.args.vol_or_crash and not self.args.vol_nospawn ) or bos.path.exists(vol.realpath) ): pass elif "assert_root" in vol.flags or self.args.vol_or_crash: t = "ERROR: volume [/%s] root folder %r does not exist on server HDD; will now crash due to volflag 'assert_root'" self.log(t % (vol.vpath, vol.realpath), 1) errors = True else: t = "WARNING: volume [/%s] root folder %r does not exist on server HDD; volume will be unavailable due to volflag 'nospawn'" self.log(t % (vol.vpath, vol.realpath), 3) dropvols.append(vol) if errors: sys.exit(1) for vol in dropvols: vol.axs = AXS() vol.uaxs = {} vfs.all_vols.pop(vol.vpath, None) vfs.all_nodes.pop(vol.vpath, None) for zv in vfs.all_nodes.values(): try: zv.all_aps.remove(vol.realpath) zv.all_vps.remove(vol.vpath) # pointless but might as well: zv.all_vols.pop(vol.vpath) zv.all_nodes.pop(vol.vpath) except: pass zs = next((x for x, y in zv.nodes.items() if y == vol), "") if zs: zv.nodes.pop(zs) vol.realpath = "" promote = [] demote = [] for vol in vfs.all_vols.values(): if not vol.realpath: continue hid = self.hid_cache.get(vol.realpath) if not hid: zb = hashlib.sha512(afsenc(vol.realpath)).digest() hid = base64.b32encode(zb).decode("ascii").lower() self.hid_cache[vol.realpath] = hid vflag = vol.flags.get("hist") if vflag == "-": pass elif vflag: vflag = os.path.expandvars(os.path.expanduser(vflag)) vol.histpath = vol.dbpath = uncyg(vflag) if WINDOWS else vflag elif self.args.hist: for nch in range(len(hid)): hpath = os.path.join(self.args.hist, hid[: nch + 1]) bos.makedirs(hpath) powner = os.path.join(hpath, "owner.txt") try: with open(powner, "rb") as f: owner = f.read().rstrip() except: owner = None me = afsenc(vol.realpath).rstrip() if owner not in [None, me]: continue if owner is None: with open(powner, "wb") as f: f.write(me) vol.histpath = vol.dbpath = hpath break vol.histpath = absreal(vol.histpath) for vol in vfs.all_vols.values(): if not vol.realpath: continue hid = self.hid_cache[vol.realpath] vflag = vol.flags.get("dbpath") if vflag == "-": pass elif vflag: vflag = os.path.expandvars(os.path.expanduser(vflag)) vol.dbpath = uncyg(vflag) if WINDOWS else vflag elif self.args.dbpath: for nch in range(len(hid)): hpath = os.path.join(self.args.dbpath, hid[: nch + 1]) bos.makedirs(hpath) powner = os.path.join(hpath, "owner.txt") try: with open(powner, "rb") as f: owner = f.read().rstrip() except: owner = None me = afsenc(vol.realpath).rstrip() if owner not in [None, me]: continue if owner is None: with open(powner, "wb") as f: f.write(me) vol.dbpath = hpath break vol.dbpath = absreal(vol.dbpath) if vol.dbv: if bos.path.exists(os.path.join(vol.dbpath, "up2k.db")): promote.append(vol) vol.dbv = None else: demote.append(vol) # discard jump-vols for zv in demote: vfs.all_vols.pop(zv.vpath) if promote: ta = [ "\n the following jump-volumes were generated to assist the vfs.\n As they contain a database (probably from v0.11.11 or older),\n they are promoted to full volumes:" ] for vol in promote: ta.append(" /%s (%s) (%s)" % (vol.vpath, vol.realpath, vol.dbpath)) self.log("\n\n".join(ta) + "\n", c=3) rhisttab = {} vfs.histtab = {} for zv in vfs.all_vols.values(): histp = zv.histpath is_shr = shr and zv.vpath.split("/")[0] == shr if histp and not is_shr and histp in rhisttab: zv2 = rhisttab[histp] t = "invalid config; multiple volumes share the same histpath (database+thumbnails location):\n histpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]" t = t % (histp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath) self.log(t, 1) raise Exception(t) rhisttab[histp] = zv vfs.histtab[zv.realpath] = histp rdbpaths = {} vfs.dbpaths = {} for zv in vfs.all_vols.values(): dbp = zv.dbpath is_shr = shr and zv.vpath.split("/")[0] == shr if dbp and not is_shr and dbp in rdbpaths: zv2 = rdbpaths[dbp] t = "invalid config; multiple volumes share the same dbpath (database location):\n dbpath: %s\n volume 1: /%s [%s]\n volume 2: /%s [%s]" t = t % (dbp, zv2.vpath, zv2.realpath, zv.vpath, zv.realpath) self.log(t, 1) raise Exception(t) rdbpaths[dbp] = zv vfs.dbpaths[zv.realpath] = dbp for vol in vfs.all_vols.values(): use = False for k in ["zipmaxn", "zipmaxs"]: try: zs = vol.flags[k] except: zs = getattr(self.args, k) if zs in ("", "0"): vol.flags[k] = 0 continue zf = unhumanize(zs) vol.flags[k + "_v"] = zf if zf: use = True if use: vol.flags["zipmax"] = True for vol in vfs.all_vols.values(): lim = Lim(self.args, self.log_func) use = False if vol.flags.get("nosub"): use = True lim.nosub = True zs = vol.flags.get("df") or self.args.df or "" if zs not in ("", "0"): use = True try: _ = float(zs) zs = "%sg" % (zs,) except: pass lim.dfl = unhumanize(zs) zs = vol.flags.get("sz") if zs: use = True lim.smin, lim.smax = [unhumanize(x) for x in zs.split("-")] zs = vol.flags.get("rotn") if zs: use = True lim.rotn, lim.rotl = [int(x) for x in zs.split(",")] zs = vol.flags.get("rotf") if zs: use = True lim.set_rotf(zs, vol.flags.get("rotf_tz") or "UTC") zs = vol.flags.get("maxn") if zs: use = True lim.nmax, lim.nwin = [int(x) for x in zs.split(",")] zs = vol.flags.get("maxb") if zs: use = True lim.bmax, lim.bwin = [unhumanize(x) for x in zs.split(",")] zs = vol.flags.get("vmaxb") if zs: use = True lim.vbmax = unhumanize(zs) zs = vol.flags.get("vmaxn") if zs: use = True lim.vnmax = unhumanize(zs) if use: vol.lim = lim if self.args.no_robots: for vol in vfs.all_nodes.values(): # volflag "robots" overrides global "norobots", allowing indexing by search engines for this vol if not vol.flags.get("robots"): vol.flags["norobots"] = True for vol in vfs.all_nodes.values(): if self.args.no_vthumb: vol.flags["dvthumb"] = True if self.args.no_athumb: vol.flags["dathumb"] = True if self.args.no_thumb or vol.flags.get("dthumb", False): vol.flags["dthumb"] = True vol.flags["dvthumb"] = True vol.flags["dathumb"] = True vol.flags["dithumb"] = True have_fk = False for vol in vfs.all_nodes.values(): fk = vol.flags.get("fk") fka = vol.flags.get("fka") if fka and not fk: fk = fka if fk: fk = 8 if fk is True else int(fk) if fk > 72: t = "max filekey-length is 72; volume /%s specified %d (anything higher than 16 is pointless btw)" raise Exception(t % (vol.vpath, fk)) vol.flags["fk"] = fk have_fk = True dk = vol.flags.get("dk") dks = vol.flags.get("dks") dky = vol.flags.get("dky") if dks is not None and dky is not None: t = "WARNING: volume /%s has both dks and dky enabled; this is too yolo and not permitted" raise Exception(t % (vol.vpath,)) if dks and not dk: dk = dks if dky and not dk: dk = dky if dk: vol.flags["dk"] = int(dk) if dk is not True else 8 if have_fk and re.match(r"^[0-9\.]+$", self.args.fk_salt): self.log("filekey salt: {}".format(self.args.fk_salt)) fk_len = len(self.args.fk_salt) if have_fk and fk_len < 14: t = "WARNING: filekeys are enabled, but the salt is only %d chars long; %d or longer is recommended. Either specify a stronger salt using --fk-salt or delete this file and restart copyparty: %s" zs = os.path.join(E.cfg, "fk-salt.txt") self.log(t % (fk_len, 16, zs), 3) for vol in vfs.all_nodes.values(): if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags: vol.flags["gz"] = False # def.pk if "scan" in vol.flags: vol.flags["scan"] = int(vol.flags["scan"]) elif self.args.re_maxage: vol.flags["scan"] = self.args.re_maxage self.args.have_unlistc = False all_mte = {} errors = False free_umask = False have_reflink = False for vol in vfs.all_nodes.values(): if os.path.isfile(vol.realpath): vol.flags["is_file"] = True vol.flags["d2d"] = True if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa: vol.flags["e2ds"] = True if self.args.e2d or "e2ds" in vol.flags: vol.flags["e2d"] = True for ga, vf in [ ["no_hash", "nohash"], ["no_idx", "noidx"], ["og_ua", "og_ua"], ["srch_excl", "srch_excl"], ]: if vf in vol.flags: ptn = re.compile(vol.flags.pop(vf)) else: ptn = getattr(self.args, ga) if ptn: vol.flags[vf] = ptn for ga, vf in vf_bmap().items(): if getattr(self.args, ga): vol.flags[vf] = True for ve, vd in ( ("nodotsrch", "dotsrch"), ("sb_lg", "no_sb_lg"), ("sb_md", "no_sb_md"), ): if ve in vol.flags: vol.flags.pop(vd, None) for ga, vf in vf_vmap().items(): if vf not in vol.flags: vol.flags[vf] = getattr(self.args, ga) zs = "forget_ip gid nrand tail_who th_qv th_qvx th_spec_p u2abort u2ow uid unp_who ups_who zip_who" for k in zs.split(): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) zs = "aconvt convt tail_fd tail_rate tail_tmax" for k in zs.split(): if k in vol.flags: vol.flags[k] = float(vol.flags[k]) for k in ("mv_re", "rm_re"): try: zs1, zs2 = vol.flags[k + "try"].split("/") vol.flags[k + "_t"] = float(zs1) vol.flags[k + "_r"] = float(zs2) except: t = 'volume "/%s" has invalid %stry [%s]' raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try"))) for k in ("chmod_d", "chmod_f"): is_d = k == "chmod_d" zs = vol.flags.get(k, "") if not zs and is_d: zs = "755" if not zs: vol.flags.pop(k, None) continue if not re.match("^[0-7]{3,4}$", zs): t = "config-option '%s' must be a three- or four-digit octal value such as [0755] or [644] but the value was [%s]" t = t % (k, zs) self.log(t, 1) raise Exception(t) zi = int(zs, 8) vol.flags[k] = zi if (is_d and zi != 0o755) or not is_d: free_umask = True vol.flags.pop("chown", None) if vol.flags["uid"] != -1 or vol.flags["gid"] != -1: vol.flags["chown"] = True vol.flags.pop("fperms", None) if "chown" in vol.flags or vol.flags.get("chmod_f"): vol.flags["fperms"] = True if vol.lim: vol.lim.chmod_d = vol.flags["chmod_d"] vol.lim.chown = "chown" in vol.flags vol.lim.uid = vol.flags["uid"] vol.lim.gid = vol.flags["gid"] vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"]) if not enshare: vol.flags["shr_who"] = self.args.shr_who = "no" if vol.flags.get("og"): self.args.uqe = True if "unlistcr" in vol.flags or "unlistcw" in vol.flags: self.args.have_unlistc = True if "reflink" in vol.flags: have_reflink = True zs = str(vol.flags.get("tcolor", "")).lstrip("#") if len(zs) == 3: # fc5 => ffcc55 vol.flags["tcolor"] = "".join([x * 2 for x in zs]) # volflag syntax currently doesn't allow for ':' in value zs = vol.flags["put_name"] vol.flags["put_name2"] = zs.replace("{now.", "{now:.") if vol.flags.get("neversymlink"): vol.flags["hardlinkonly"] = True # was renamed if vol.flags.get("hardlinkonly"): vol.flags["hardlink"] = True for k1, k2 in IMPLICATIONS: if k1 in vol.flags: vol.flags[k2] = True for k1, k2 in UNPLICATIONS: if k1 in vol.flags: vol.flags[k2] = False dbds = "acid|swal|wal|yolo" vol.flags["dbd"] = dbd = vol.flags.get("dbd") or self.args.dbd if dbd not in dbds.split("|"): t = 'volume "/%s" has invalid dbd [%s]; must be one of [%s]' raise Exception(t % (vol.vpath, dbd, dbds)) # default tag cfgs if unset for k in ("mte", "mth", "exp_md", "exp_lg"): if k not in vol.flags: vol.flags[k] = getattr(self.args, k).copy() else: vol.flags[k] = odfusion(getattr(self.args, k), vol.flags[k]) # append additive args from argv to volflags hooks = "xbu xau xiu xbc xac xbr xar xbd xad xm xban".split() for name in "ext_th mtp on404 on403".split() + hooks: self._read_volflag( vol.vpath, vol.flags, name, getattr(self.args, name), True ) for hn in hooks: cmds = vol.flags.get(hn) if not cmds: continue ncmds = [] for cmd in cmds: hfs = [] ocmd = cmd while "," in cmd[:6]: zs, cmd = cmd.split(",", 1) hfs.append(zs) if "c" in hfs and "f" in hfs: t = "cannot combine flags c and f; removing f from eventhook [{}]" self.log(t.format(ocmd), 1) hfs = [x for x in hfs if x != "f"] ocmd = ",".join(hfs + [cmd]) if "c" not in hfs and "f" not in hfs and hn == "xban": hfs = ["c"] + hfs ocmd = ",".join(hfs + [cmd]) ncmds.append(ocmd) vol.flags[hn] = ncmds ext_th = vol.flags["ext_th_d"] = {} etv = "(?)" try: for etv in vol.flags.get("ext_th") or []: k, v = etv.split("=") ext_th[k] = v except: t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s" self.log(t % (vol.vpath, etv), 3) zsl = [x.strip() for x in vol.flags["rw_edit"].split(",")] zsl = [x for x in zsl if x] vol.flags["rw_edit"] = ",".join(zsl) vol.flags["rw_edit_set"] = set(x for x in zsl if x) emb_all = vol.flags["emb_all"] = set() zsl1 = [x for x in vol.flags["preadmes"].split(",") if x] zsl2 = [x for x in vol.flags["readmes"].split(",") if x] zsl3 = list(set([x.lower() for x in zsl1])) zsl4 = list(set([x.lower() for x in zsl2])) emb_all.update(zsl3) emb_all.update(zsl4) vol.flags["emb_mds"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]] zsl1 = [x for x in vol.flags["prologues"].split(",") if x] zsl2 = [x for x in vol.flags["epilogues"].split(",") if x] zsl3 = list(set([x.lower() for x in zsl1])) zsl4 = list(set([x.lower() for x in zsl2])) emb_all.update(zsl3) emb_all.update(zsl4) vol.flags["emb_lgs"] = [[0, zsl1, zsl3], [1, zsl2, zsl4]] zs = str(vol.flags.get("html_head") or "") if zs and zs[:1] in "%@": vol.flags["html_head_d"] = zs head_s = str(vol.flags.get("html_head_s") or "") else: zs2 = str(vol.flags.get("html_head_s") or "") if zs2 and zs: head_s = "%s\n%s\n" % (zs2.strip(), zs.strip()) else: head_s = zs2 or zs if head_s and not head_s.endswith("\n"): head_s += "\n" zs = "X-Content-Type-Options: nosniff\r\n" if "norobots" in vol.flags: head_s += META_NOBOTS zs += "X-Robots-Tag: noindex, nofollow\r\n" if self.args.http_vary: zs += "Vary: %s\r\n" % (self.args.http_vary,) vol.flags["oh_g"] = zs + "\r\n" if "noscript" in vol.flags: zs += "Content-Security-Policy: script-src 'none';\r\n" vol.flags["oh_f"] = zs + "\r\n" ico_url = vol.flags.get("ufavico") if ico_url: ico_h = "" ico_ext = ico_url.split("?")[0].split(".")[-1].lower() if ico_ext in FAVICON_MIMES: zs = '\n' ico_h = zs % (FAVICON_MIMES[ico_ext], ico_url) elif ico_ext == "ico": zs = '\n' ico_h = zs % (ico_url,) if ico_h: vol.flags["ufavico_h"] = ico_h head_s += ico_h if head_s: vol.flags["html_head_s"] = head_s else: vol.flags.pop("html_head_s", None) if not vol.flags.get("html_head_d"): vol.flags.pop("html_head_d", None) vol.check_landmarks() if vol.flags.get("db_xattr"): self.args.have_db_xattr = True zs = str(vol.flags["db_xattr"]) neg = zs.startswith("~~") if neg: zs = zs[2:] zsl = [x.strip() for x in zs.split(",")] zsl = [x for x in zsl if x] if neg: vol.flags["db_xattr_no"] = set(zsl) else: vol.flags["db_xattr_yes"] = zsl # d2d drops all database features for a volume for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]: if not vol.flags.get(grp, False): continue vol.flags["d2t"] = True vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)} # d2ds drops all onboot scans for a volume for grp, rm in [["d2ds", "e2ds"], ["d2ts", "e2ts"]]: if not vol.flags.get(grp, False): continue vol.flags["d2ts"] = True vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)} # mt* needs e2t so drop those too for grp, rm in [["e2t", "mt"]]: if vol.flags.get(grp, False): continue vol.flags = { k: v for k, v in vol.flags.items() if not k.startswith(rm) or k == "mte" } for grp, rm in [["d2v", "e2v"]]: if not vol.flags.get(grp, False): continue vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)} ints = ["lifetime"] for k in list(vol.flags): if k in ints: vol.flags[k] = int(vol.flags[k]) if "e2d" not in vol.flags: zs = "lifetime rss" drop = [x for x in zs.split() if x in vol.flags] zs = "xau xiu" drop += [x for x in zs.split() if vol.flags.get(x)] for k in drop: t = 'cannot enable [%s] for volume "/%s" because this requires one of the following: e2d / e2ds / e2dsa (either as volflag or global-option)' self.log(t % (k, vol.vpath), 1) vol.flags.pop(k) zi = vol.flags.get("lifetime") or 0 zi2 = time.time() // (86400 * 365) zi3 = zi2 * 86400 * 365 if zi < 0 or zi > zi3: t = "the lifetime of volume [/%s] (%d) exceeds max value (%d years; %d)" t = t % (vol.vpath, zi, zi2, zi3) self.log(t, 1) raise Exception(t) if ( "dedup" in vol.flags and "reflink" not in vol.flags and vol.flags["apnd_who"] != "no" ): vol.flags["apnd_who"] = "ndd" # verify tags mentioned by -mt[mp] are used by -mte local_mtp = {} local_only_mtp = {} tags = vol.flags.get("mtp", []) + vol.flags.get("mtm", []) tags = [x.split("=")[0] for x in tags] tags = [y for x in tags for y in x.split(",")] for a in tags: local_mtp[a] = True local = True for b in self.args.mtp or []: b = b.split("=")[0] if a == b: local = False if local: local_only_mtp[a] = True local_mte = ODict() for a in vol.flags.get("mte", {}).keys(): local = True all_mte[a] = True local_mte[a] = True for b in self.args.mte.keys(): if not a or not b: continue if a == b: local = False for mtp in local_only_mtp: if mtp not in local_mte: t = 'volume "/{}" defines metadata tag "{}", but doesnt use it in "-mte" (or with "cmte" in its volflags)' self.log(t.format(vol.vpath, mtp), 1) errors = True mte = vol.flags.get("mte") or {} up_m = [x for x in UP_MTE_MAP if x in mte] up_q = [UP_MTE_MAP[x] for x in up_m] zs = "select %s from up where rd=? and fn=?" % (", ".join(up_q),) vol.flags["ls_q_m"] = (zs if up_m else "", up_m) vfs.all_fvols = { zs: vol for zs, vol in vfs.all_vols.items() if "is_file" in vol.flags } for vol in vfs.all_nodes.values(): if not vol.flags.get("is_file"): continue zs = "og opds xlink" for zs in zs.split(): vol.flags.pop(zs, None) for vol in vfs.all_vols.values(): if not vol.realpath or vol.flags.get("is_file"): continue ccs = vol.flags["casechk"][:1].lower() if ccs in ("y", "n"): if ccs == "y": vol.flags["bcasechk"] = True continue try: bos.makedirs(vol.realpath, vf=vol.flags) files = os.listdir(vol.realpath) for fn in files: fn2 = fn.lower() if fn == fn2: fn2 = fn.upper() if fn == fn2 or fn2 in files: continue is_ci = os.path.exists(os.path.join(vol.realpath, fn2)) ccs = "y" if is_ci else "n" break if ccs not in ("y", "n"): ap = os.path.join(vol.realpath, "casechk") open(ap, "wb").close() ccs = "y" if os.path.exists(ap[:-1] + "K") else "n" os.unlink(ap) except Exception as ex: if ANYWIN: zs = "Windows" ccs = "y" elif MACOS: zs = "Macos" ccs = "y" else: zs = "Linux" ccs = "n" t = "unable to determine if filesystem at %r is case-insensitive due to %r; assuming casechk=%s due to %s" self.log(t % (vol.realpath, ex, ccs, zs), 3) vol.flags["casechk"] = ccs if ccs == "y": vol.flags["bcasechk"] = True tags = self.args.mtp or [] tags = [x.split("=")[0] for x in tags] tags = [y for x in tags for y in x.split(",")] for mtp in tags: if mtp not in all_mte: t = 'metadata tag "{}" is defined by "-mtm" or "-mtp", but is not used by "-mte" (or by any "cmte" volflag)' self.log(t.format(mtp), 1) errors = True for vol in vfs.all_vols.values(): re1: Optional[re.Pattern] = vol.flags.get("srch_excl") excl = [re1.pattern] if re1 else [] vpaths = [] vtop = vol.vpath for vp2 in vfs.all_vols.keys(): if vp2.startswith((vtop + "/").lstrip("/")) and vtop != vp2: vpaths.append(re.escape(vp2[len(vtop) :].lstrip("/"))) if vpaths: excl.append("^(%s)/" % ("|".join(vpaths),)) vol.flags["srch_re_dots"] = re.compile("|".join(excl or ["^$"])) excl.extend([r"^\.", r"/\."]) vol.flags["srch_re_nodot"] = re.compile("|".join(excl)) have_daw = False for vol in vfs.all_nodes.values(): daw = vol.flags.get("daw") or self.args.daw if daw: vol.flags["daw"] = True have_daw = True if have_daw and self.args.no_dav: t = 'volume "/{}" has volflag "daw" (webdav write-access), but --no-dav is set' self.log(t, 1) errors = True if self.args.smb and self.ah.on and acct: self.log("--smb can only be used when --ah-alg is none", 1) errors = True for vol in vfs.all_nodes.values(): for k in list(vol.flags.keys()): if re.match("^-[^-]+$", k): vol.flags.pop(k) zs = k[1:] if zs in vol.flags: vol.flags.pop(k[1:]) else: t = "WARNING: the config for volume [/%s] tried to remove volflag [%s] by specifying [%s] but that volflag was not already set" self.log(t % (vol.vpath, zs, k), 3) if vol.flags.get("dots"): for name in vol.axs.uread: vol.axs.udot.add(name) if errors: sys.exit(1) setattr(self.args, "free_umask", free_umask) if free_umask: os.umask(0) vfs.bubble_flags() have_e2d = False have_e2t = False have_dedup = False have_symdup = False unsafe_dedup = [] t = "volumes and permissions:\n" for zv in vfs.all_vols.values(): if not self.warn_anonwrite or verbosity < 5: break if enshare and (zv.vpath == shr or zv.vpath.startswith(shrs)): continue t += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(zv.vpath, zv.realpath) for txt, attr in [ [" read", "uread"], [" write", "uwrite"], [" move", "umove"], ["delete", "udel"], [" dots", "udot"], [" get", "uget"], [" upGet", "upget"], [" html", "uhtml"], ["uadmin", "uadmin"], ]: u = list(sorted(getattr(zv.axs, attr))) if u == ["*"] and acct: u = ["\033[35monly-anonymous\033[0m"] elif "*" in u: u = ["\033[35meverybody\033[0m"] if not u: u = ["\033[36m--none--\033[0m"] u = ", ".join(u) t += "\n| {}: {}".format(txt, u) if "e2d" in zv.flags: have_e2d = True if "e2t" in zv.flags: have_e2t = True if "dedup" in zv.flags: have_dedup = True if "hardlink" not in zv.flags and "reflink" not in zv.flags: have_symdup = True if "e2d" not in zv.flags: unsafe_dedup.append("/" + zv.vpath) t += "\n" if have_symdup and self.args.fika: t = "WARNING: disabling fika due to symlink-based dedup in at least one volume; uploads/deletes will be blocked during filesystem-indexing. Consider --reflink or --hardlink" # self.args.fika = self.args.fika.replace("m", "").replace("d", "") # probably not enough self.args.fika = "" if self.warn_anonwrite and verbosity > 4: if not self.args.no_voldump: self.log(t) if have_e2d or self.args.have_idp_hdrs: t = self.chk_sqlite_threadsafe() if t: self.log("\n\033[{}\033[0m\n".format(t)) if have_e2d: if not have_e2t: t = "hint: enable multimedia indexing (artist/title/...) with argument -e2ts" self.log(t, 6) else: t = "hint: enable searching and upload-undo with argument -e2dsa" self.log(t, 6) if unsafe_dedup: t = "WARNING: symlink-based deduplication is enabled for some volumes, but without indexing. Please enable -e2dsa and/or --hardlink to avoid problems when moving/renaming files. Affected volumes: %s" self.log(t % (", ".join(unsafe_dedup)), 3) elif not have_dedup: t = "hint: enable upload deduplication with --dedup (but see readme for consequences)" self.log(t, 6) zv, _ = vfs.get("/", "*", False, False) zs = zv.realpath.lower() if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"): t = "you are sharing a system directory: {}\n" self.log(t.format(zv.realpath), c=1) try: zv, _ = vfs.get("", "*", False, True, err=999) if self.warn_anonwrite and verbosity > 4 and os.getcwd() == zv.realpath: t = "anyone can write to the current directory: {}\n" self.log(t.format(zv.realpath), c=1) self.warn_anonwrite = False except Pebkac: self.warn_anonwrite = True self.idp_warn = [] self.idp_err = [] for idp_vp in self.idp_vols: idp_vn, _ = vfs.get(idp_vp, "*", False, False) idp_vp0 = idp_vn.vpath0 sigils = set(PTN_SIGIL.findall(idp_vp0)) if len(sigils) > 1: t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s' self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils))) continue sigil = sigils.pop() par_vp = idp_vp while par_vp: par_vp = vsplit(par_vp)[0] par_vn, _ = vfs.get(par_vp, "*", False, False) if sigil in par_vn.vpath0: continue # parent was spawned for and by same user oth_read = [] oth_write = [] for usr in par_vn.axs.uread: if usr not in idp_vn.axs.uread: oth_read.append(usr) for usr in par_vn.axs.uwrite: if usr not in idp_vn.axs.uwrite: oth_write.append(usr) if "*" in oth_read: taxs = "WORLD-READABLE" elif "*" in oth_write: taxs = "WORLD-WRITABLE" elif oth_read: taxs = "READABLE BY %r" % (oth_read,) elif oth_write: taxs = "WRITABLE BY %r" % (oth_write,) else: break # no sigil; not idp; safe to stop t = '\nWARNING: IdP-volume "/%s" created by "/%s" has parent/grandparent "/%s" and would be %s' self.idp_err.append(t % (idp_vp, idp_vp0, par_vn.vpath, taxs)) if self.idp_warn: t = "WARNING! Some IdP volumes include multiple IdP placeholders; this is too complex to automatically determine if safe or not. To ensure that no users gain unintended access, please use only a single placeholder for each IdP volume." self.log(t + "".join(self.idp_warn), 1) if self.idp_err: t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume." self.log(t + "".join(self.idp_err), 1) if have_reflink: t = "WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead." if not sys.platform.startswith("linux"): self.log(t % "your OS is not Linux", 1) self.vfs = vfs self.acct = acct self.defpw = defpw self.grps = grps self.iacct = {v: k for k, v in acct.items()} self.cfg_files_loaded = cfg_files_loaded self.load_sessions() self.re_pwd = None pwds = [re.escape(x) for x in self.iacct.keys()] pwds.extend(list(self.sesa)) if self.args.usernames: pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x]) if pwds: if self.ah.on: zs = r"(\[H\] %s:.*|[?&]%s=)([^&]+)" zs = zs % (self.args.pw_hdr, self.args.pw_urlp) else: zs = r"(\[H\] %s:.*|=)(" % (self.args.pw_hdr,) zs += "|".join(pwds) + r")([]&; ]|$)" self.re_pwd = re.compile(zs) # to ensure it propagates into tcpsrv with mp on if self.args.mime: for zs in self.args.mime: ext, mime = zs.split("=", 1) MIMES[ext] = mime EXTS.update({v: k for k, v in MIMES.items()}) if enshare: # hide shares from controlpanel vfs.all_vols = { x: y for x, y in vfs.all_vols.items() if x != shr and not x.startswith(shrs) } assert db and cur and cur2 and shv # type: ignore for row in cur.execute("select * from sh"): s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row shn = shv.nodes.get(s_k, None) if not shn: continue try: s_vfs, s_rem = vfs.get( s_vp, s_un, "r" in s_pr, "w" in s_pr, "m" in s_pr, "d" in s_pr ) except Exception as ex: t = "removing share [%s] by [%s] to [%s] due to %r" self.log(t % (s_k, s_un, s_vp, ex), 3) shv.nodes.pop(s_k) continue fns = [] if s_nf: q = "select vp from sf where k = ?" for (s_fn,) in cur2.execute(q, (s_k,)): fns.append(s_fn) shn.shr_files = set(fns) shn.ls = shn._ls_shr shn.canonical = shn._canonical_shr shn.dcanonical = shn._dcanonical_shr else: shn.ls = shn._ls shn.canonical = shn._canonical shn.dcanonical = shn._dcanonical shn.shr_owner = s_un shn.shr_src = (s_vfs, s_rem) shn.realpath = s_vfs.canonical(s_rem) o_vn, _ = shn._get_share_src("") shn.lim = o_vn.lim shn.flags = o_vn.flags.copy() shn.dbpath = o_vn.dbpath shn.histpath = o_vn.histpath # root.all_aps doesn't include any shares, so make a copy where the # share appears in all abspaths it can provide (for example for chk_ap) ap = shn.realpath if not ap.endswith(os.sep): ap += os.sep shn.shr_all_aps = [(x, y[:]) for x, y in vfs.all_aps] exact = False for ap2, vns in shn.shr_all_aps: if ap == ap2: exact = True if ap2.startswith(ap): try: vp2 = vjoin(s_rem, ap2[len(ap) :]) vn2, _ = s_vfs.get(vp2, "*", False, False) if vn2 == s_vfs or vn2.dbv == s_vfs: vns.append(shn) except: pass if not exact: shn.shr_all_aps.append((ap, [shn])) shn.shr_all_aps.sort(key=lambda x: len(x[0]), reverse=True) if self.args.shr_v: t = "mapped %s share [%s] by [%s] => [%s] => [%s]" self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath)) # transplant shadowing into shares for vn in shv.nodes.values(): svn, srem = vn.shr_src # type: ignore if srem: continue # free branch, safe ap = svn.canonical(srem) if bos.path.isfile(ap): continue # also fine for zs in svn.nodes.keys(): # hide subvolume vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), self.vf0()) cur2.close() cur.close() db.close() self.js_ls = {} self.js_htm = {} for vp, vn in self.vfs.all_nodes.items(): if enshare and vp.startswith(shrs): continue # propagates later in this func vf = vn.flags vn.js_ls = { "idx": "e2d" in vf, "itag": "e2t" in vf, "dlni": "dlni" in vf, "dgrid": "grid" in vf, "dnsort": "nsort" in vf, "dhsortn": vf["hsortn"], "dsort": vf["sort"], "dcrop": vf["crop"], "dth3x": vf["th3x"], "u2ts": vf["u2ts"], "shr_who": vf["shr_who"], "frand": bool(vf.get("rand")), "lifetime": vf.get("lifetime") or 0, "unlist": vf.get("unlist") or "", "sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"), "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"), "rw_edit": vf["rw_edit"], } if "ufavico_h" in vf: vn.js_ls["ufavico"] = vf["ufavico_h"] js_htm = { "SPINNER": self.args.spinner, "s_name": self.args.bname, "idp_login": self.args.idp_login, "have_up2k_idx": "e2d" in vf, "have_acode": not self.args.no_acode, "have_c2flac": self.args.allow_flac, "have_c2wav": self.args.allow_wav, "have_shr": self.args.shr, "shr_who": vf["shr_who"], "have_zip": not self.args.no_zip, "have_zls": not self.args.no_zls, "have_mv": not self.args.no_mv, "have_del": not self.args.no_del, "have_unpost": int(self.args.unpost), "have_emp": int(self.args.emp), "md_no_br": int(vf.get("md_no_br") or 0), "ext_th": vf.get("ext_th_d") or {}, "sb_lg": vn.js_ls["sb_lg"], "sb_md": vn.js_ls["sb_md"], "sba_md": vf.get("md_sba") or "", "sba_lg": vf.get("lg_sba") or "", "rw_edit": vf["rw_edit"], "txt_ext": self.args.textfiles.replace(",", " "), "def_hcols": list(vf.get("mth") or []), "unlist0": vf.get("unlist") or "", "see_dots": self.args.see_dots, "dqdel": self.args.qdel, "dlni": vn.js_ls["dlni"], "dgrid": vn.js_ls["dgrid"], "dgsel": "gsel" in vf, "dnsort": "nsort" in vf, "dhsortn": vf["hsortn"], "dsort": vf["sort"], "dcrop": vf["crop"], "dth3x": vf["th3x"], "drcm": self.args.rcm, "dvol": self.args.au_vol, "idxh": int(self.args.ih), "dutc": not self.args.localtime, "dfszf": self.args.ui_filesz.strip("-"), "themes": self.args.themes, "turbolvl": self.args.turbo, "nosubtle": self.args.nosubtle, "u2j": self.args.u2j, "u2sz": self.args.u2sz, "u2ts": vf["u2ts"], "u2ow": vf["u2ow"], "frand": bool(vf.get("rand")), "lifetime": vn.js_ls["lifetime"], "u2sort": self.args.u2sort, } zs = "ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi" for zs in zs.split(): if vf.get(zs): js_htm[zs] = 1 zs = "notooltips" for zs in zs.split(): if getattr(self.args, zs, False): js_htm[zs] = 1 zs = "up_site" for zs in zs.split(): zs2 = getattr(self.args, zs, "") if zs2: js_htm[zs] = zs2 vn.js_htm = json_hesc(json.dumps(js_htm)) vols = list(vfs.all_nodes.values()) if enshare: assert shv # type: ignore # !rm for vol in shv.nodes.values(): if vol.vpath not in vfs.all_nodes: self.log("BUG: /%s not in all_nodes" % (vol.vpath,), 1) vols.append(vol) if shr in vfs.all_nodes: t = "invalid config: a volume is overlapping with the --shr global-option (/%s)" t = t % (shr,) self.log(t, 1) raise Exception(t) for vol in vols: dbv = vol.get_dbv("")[0] vol.js_ls = vol.js_ls or dbv.js_ls or {} vol.js_htm = vol.js_htm or dbv.js_htm or "{}" zs = str(vol.flags.get("tcolor") or self.args.tcolor) vol.flags["tcolor"] = zs.lstrip("#") def setup_auth_ord(self) -> None: ao = [x.strip() for x in self.args.auth_ord.split(",")] if "idp" in ao: zi = ao.index("idp") ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:] zsl = "pw idp-h idp-hm ipu".split() pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl] self.args.ao_idp_before_pw = min(h, hm) < pw self.args.ao_h_before_hm = h < hm self.args.ao_ipu_wins = ipu == 0 self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs def load_idp_db(self, quiet=False) -> None: # mutex me level = self.args.idp_store if level < 2 or not self.args.have_idp_hdrs: return assert sqlite3 # type: ignore # !rm db = sqlite3.connect(self.args.idp_db) cur = db.cursor() from_cache = cur.execute("select un, gs from us").fetchall() cur.close() db.close() old_accs = self.idp_accs.copy() self.idp_accs.clear() self.idp_usr_gh.clear() gsep = self.args.idp_gsep groupless = (None, [""]) n = [] for uname, gname in from_cache: if level < 3: if uname in self.idp_accs: continue if old_accs.get(uname) in groupless: gname = "" gnames = [x.strip() for x in gsep.split(gname)] gnames.sort() self.idp_accs[uname] = gnames n.append(uname) if n and not quiet: t = ", ".join(n[:9]) if len(n) > 9: t += "..." self.log("found %d IdP users in db (%s)" % (len(n), t)) def load_sessions(self, quiet=False) -> None: # mutex me if self.args.no_ses: self.ases = {} self.sesa = {} return assert sqlite3 # type: ignore # !rm ases = {} blen = (self.args.ses_len // 4) * 4 # 3 bytes in 4 chars blen = (blen * 3) // 4 # bytes needed for ses_len chars db = sqlite3.connect(self.args.ses_db) cur = db.cursor() for uname, sid in cur.execute("select un, si from us"): if uname in self.acct: ases[uname] = sid n = [] q = "insert into us values (?,?,?)" accs = list(self.acct) if self.args.have_idp_hdrs and self.args.idp_cookie: accs.extend(self.idp_accs.keys()) for uname in accs: if uname not in ases: sid = ub64enc(os.urandom(blen)).decode("ascii") cur.execute(q, (uname, sid, int(time.time()))) ases[uname] = sid n.append(uname) if n: db.commit() cur.close() db.close() self.ases = ases self.sesa = {v: k for k, v in ases.items()} if n and not quiet: t = ", ".join(n[:3]) if len(n) > 3: t += "..." self.log("added %d new sessions (%s)" % (len(n), t)) def forget_session(self, broker: Optional["BrokerCli"], uname: str) -> None: with self.mutex: self._forget_session(uname) if broker: broker.ask("_reload_sessions").get() def _forget_session(self, uname: str) -> None: if self.args.no_ses: return assert sqlite3 # type: ignore # !rm db = sqlite3.connect(self.args.ses_db) cur = db.cursor() cur.execute("delete from us where un = ?", (uname,)) db.commit() cur.close() db.close() self.sesa.pop(self.ases.get(uname, ""), "") self.ases.pop(uname, "") def chpw(self, broker: Optional["BrokerCli"], uname, pw) -> tuple[bool, str]: if not self.args.chpw: return False, "feature disabled in server config" if uname == "*" or uname not in self.defpw: return False, "not logged in" if uname in self.args.chpw_no: return False, "not allowed for this account" if len(pw) < self.args.chpw_len: t = "minimum password length: %d characters" return False, t % (self.args.chpw_len,) if self.args.usernames: pw = "%s:%s" % (uname, pw) hpw = self.ah.hash(pw) if self.ah.on else pw if hpw == self.acct[uname]: return False, "that's already your password my dude" if hpw in self.iacct or hpw in self.sesa: return False, "password is taken" with self.mutex: ap = self.args.chpw_db if not bos.path.exists(ap): pwdb = {} else: jtxt = read_utf8(self.log, ap, True) pwdb = json.loads(jtxt) if jtxt.strip() else {} pwdb = [x for x in pwdb if x[0] != uname] pwdb.append((uname, self.defpw[uname], hpw)) with open(ap, "w", encoding="utf-8") as f: json.dump(pwdb, f, separators=(",\n", ": ")) self.log("reinitializing due to password-change for user [%s]" % (uname,)) if not broker: # only true for tests self._reload() return True, "new password OK" broker.ask("reload", False, False).get() return True, "new password OK" def setup_chpw(self, acct: dict[str, str]) -> None: ap = self.args.chpw_db if not self.args.chpw or not bos.path.exists(ap): return jtxt = read_utf8(self.log, ap, True) pwdb = json.loads(jtxt) if jtxt.strip() else {} useen = set() urst = set() uok = set() for usr, orig, mod in pwdb: useen.add(usr) if usr not in acct: # previous user, no longer known continue if acct[usr] != orig: urst.add(usr) continue uok.add(usr) acct[usr] = mod if not self.args.chpw_v: return for usr in acct: if usr not in useen: urst.add(usr) for zs in uok: urst.discard(zs) if self.args.chpw_v == 1 or (self.args.chpw_v == 2 and not urst): t = "chpw: %d changed, %d unchanged" self.log(t % (len(uok), len(urst))) return elif self.args.chpw_v == 2: t = "chpw: %d changed" % (len(uok),) if urst: t += ", \033[0munchanged:\033[35m %s" % (", ".join(list(urst))) self.log(t, 6) return msg = "" if uok: t = "\033[0mchanged: \033[32m%s" msg += t % (", ".join(list(uok)),) if urst: t = "%s\033[0munchanged: \033[35m%s" msg += t % ( ", " if msg else "", ", ".join(list(urst)), ) self.log("chpw: " + msg, 6) def setup_pwhash(self, acct: dict[str, str]) -> None: if self.args.usernames: for uname, pw in list(acct.items())[:]: if pw.startswith("+") and len(pw) == 33: continue acct[uname] = "%s:%s" % (uname, pw) self.ah = PWHash(self.args) if not self.ah.on: if self.args.ah_cli or self.args.ah_gen: t = "\n BAD CONFIG:\n cannot --ah-cli or --ah-gen without --ah-alg" raise Exception(t) return if self.args.ah_cli: self.ah.cli() sys.exit() elif self.args.ah_gen == "-": self.ah.stdin() sys.exit() elif self.args.ah_gen: print(self.ah.hash(self.args.ah_gen)) sys.exit() if not acct: return changed = False for uname, pw in list(acct.items())[:]: if pw.startswith("+") and len(pw) == 33: continue changed = True hpw = self.ah.hash(pw) acct[uname] = hpw t = "hashed password for account {}: {}" self.log(t.format(uname, hpw), 3) if not changed: return lns = [] for uname, pw in acct.items(): lns.append(" {}: {}".format(uname, pw)) t = "please use the following hashed passwords in your config:\n{}" self.log(t.format("\n".join(lns)), 3) def chk_sqlite_threadsafe(self) -> str: v = SQLITE_VER[-1:] if v == "1": # threadsafe (linux, windows) return "" if v == "2": # module safe, connections unsafe (macos) return "33m your sqlite3 was compiled with reduced thread-safety;\n database features (-e2d, -e2t) SHOULD be fine\n but MAY cause database-corruption and crashes" if v == "0": # everything unsafe return "31m your sqlite3 was compiled WITHOUT thread-safety!\n database features (-e2d, -e2t) will PROBABLY cause crashes!" return "36m cannot verify sqlite3 thread-safety; strange but probably fine" def dbg_ls(self) -> None: users = self.args.ls vol = "*" flags: Sequence[str] = [] try: users, vol = users.split(",", 1) except: pass try: vol, zf = vol.split(",", 1) flags = zf.split(",") except: pass if users == "**": users = list(self.acct.keys()) + ["*"] else: users = [users] for u in users: if u not in self.acct and u != "*": raise Exception("user not found: " + u) if vol == "*": vols = ["/" + x for x in self.vfs.all_vols] else: vols = [vol] for zs in vols: if not zs.startswith("/"): raise Exception("volumes must start with /") if zs[1:] not in self.vfs.all_vols: raise Exception("volume not found: " + zs) self.log(str({"users": users, "vols": vols, "flags": flags})) t = "/{}: read({}) write({}) move({}) del({}) dots({}) get({}) upGet({}) html({}) uadmin({})" for k, zv in self.vfs.all_vols.items(): vc = zv.axs vs = [ k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.udot, vc.uget, vc.upget, vc.uhtml, vc.uadmin, ] self.log(t.format(*vs)) flag_v = "v" in flags flag_ln = "ln" in flags flag_p = "p" in flags flag_r = "r" in flags bads = [] for v in vols: v = v[1:] vtop = "/{}/".format(v) if v else "/" for u in users: self.log("checking /{} as {}".format(v, u)) try: vn, _ = self.vfs.get(v, u, True, False, False, False, False) except: continue atop = vn.realpath safeabs = atop + os.sep g = vn.walk( vn.vpath, "", [], u, [[True, False]], 1, not self.args.no_scandir, False, False, ) for _, _, vpath, apath, files1, dirs, _ in g: fnames = [n[0] for n in files1] zsl = [vpath + "/" + n for n in fnames] if vpath else fnames vpaths = [vtop + x for x in zsl] apaths = [os.path.join(apath, n) for n in fnames] files = [(vpath + "/", apath + os.sep)] + list( [(zs1, zs2) for zs1, zs2 in zip(vpaths, apaths)] ) if flag_ln: files = [x for x in files if not x[1].startswith(safeabs)] if files: dirs[:] = [] # stop recursion bads.append(files[0][0]) if not files: continue elif flag_v: ta = [""] + [ '# user "{}", vpath "{}"\n{}'.format(u, vp, ap) for vp, ap in files ] else: ta = ["user {}, vol {}: {} =>".format(u, vtop, files[0][0])] ta += [x[1] for x in files] self.log("\n".join(ta)) if bads: self.log("\n ".join(["found symlinks leaving volume:"] + bads)) if bads and flag_p: raise Exception( "\033[31m\n [--ls] found a safety issue and prevented startup:\n found symlinks leaving volume, and strict is set\n\033[0m" ) if not flag_r: sys.exit(0) def cgen(self) -> None: ret = [ "## WARNING:", "## there will probably be mistakes in", "## commandline-args (and maybe volflags)", "", ] csv = set("i p th_covers zm_on zm_off zs_on zs_off".split()) zs = "c ihead ohead mtm mtp on403 on404 xac xad xar xau xiu xban xbc xbd xbr xbu xm" lst = set(zs.split()) askip = set("a v c vc cgen exp_lg exp_md theme".split()) t = "exp_lg exp_md ext_th_d mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot" fskip = set(t.split()) # keymap from argv to vflag amap = vf_bmap() amap.update(vf_vmap()) amap.update(vf_cmap()) vmap = {v: k for k, v in amap.items()} args = {k: v for k, v in vars(self.args).items()} pops = [] for k1, k2 in IMPLICATIONS: if args.get(k1): pops.append(k2) for pop in pops: args.pop(pop, None) if args: ret.append("[global]") for k, v in args.items(): if k in askip: continue try: v = v.pattern if k in ("idp_gsep", "tftp_lsf"): v = v[1:-1] # close enough except: pass skip = False for k2, defstr in (("mte", DEF_MTE), ("mth", DEF_MTH)): if k != k2: continue s1 = list(sorted(list(v))) s2 = list(sorted(defstr.split(","))) if s1 == s2: skip = True break v = ",".join(s1) if skip: continue if k in csv: v = ", ".join([str(za) for za in v]) try: v2 = getattr(self.dargs, k) if k == "tcolor" and len(v2) == 3: v2 = "".join([x * 2 for x in v2]) if v == v2 or v.replace(", ", ",") == v2: continue except: continue dk = " " + k.replace("_", "-") if k in lst: for ve in v: ret.append("{}: {}".format(dk, ve)) else: if v is True: ret.append(dk) elif v not in (False, None, ""): ret.append("{}: {}".format(dk, v)) ret.append("") if self.acct: ret.append("[accounts]") for u, p in self.acct.items(): ret.append(" {}: {}".format(u, p)) ret.append("") if self.grps: ret.append("[groups]") for gn, uns in self.grps.items(): ret.append(" %s: %s" % (gn, ", ".join(uns))) ret.append("") for vol in self.vfs.all_vols.values(): ret.append("[/{}]".format(vol.vpath)) ret.append(" " + vol.realpath) ret.append(" accs:") perms = { "r": "uread", "w": "uwrite", "m": "umove", "d": "udel", ".": "udot", "g": "uget", "G": "upget", "h": "uhtml", "a": "uadmin", } users = {} for pkey in perms.values(): for uname in getattr(vol.axs, pkey): try: users[uname] += 1 except: users[uname] = 1 lusers = [(v, k) for k, v in users.items()] vperms = {} for _, uname in sorted(lusers): pstr = "" for pchar, pkey in perms.items(): if uname in getattr(vol.axs, pkey): pstr += pchar if "g" in pstr and "G" in pstr: pstr = pstr.replace("g", "") pstr = pstr.replace("rwmd.a", "A") try: vperms[pstr].append(uname) except: vperms[pstr] = [uname] for pstr, uname in vperms.items(): ret.append(" {}: {}".format(pstr, ", ".join(uname))) trues = [] vals = [] for k, v in sorted(vol.flags.items()): if k in fskip: continue try: v = v.pattern except: pass try: ak = vmap[k] v2 = getattr(self.args, ak) try: v2 = v2.pattern except: pass if v2 is v: continue except: pass skip = False for k2, defstr in (("mte", DEF_MTE), ("mth", DEF_MTH)): if k != k2: continue s1 = list(sorted(list(v))) s2 = list(sorted(defstr.split(","))) if s1 == s2: skip = True break v = ",".join(s1) if skip: continue if k in lst: for ve in v: vals.append("{}: {}".format(k, ve)) elif v is True: trues.append(k) elif v is not False: vals.append("{}: {}".format(k, v)) pops = [] for k1, k2 in IMPLICATIONS: if k1 in trues: pops.append(k2) trues = [x for x in trues if x not in pops] if trues: vals.append(", ".join(trues)) if vals: ret.append(" flags:") for zs in vals: ret.append(" " + zs) ret.append("") self.log("generated config:\n\n" + "\n".join(ret)) def derive_args(args: argparse.Namespace) -> None: args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr) args.have_ipu_or_ipr = bool(args.ipu or args.ipr) def n_du_who(s: str) -> int: if s == "all": return 9 if s == "auth": return 7 if s == "w": return 5 if s == "rw": return 4 if s == "a": return 3 return 0 def n_ver_who(s: str) -> int: if s == "all": return 9 if s == "auth": return 6 if s == "a": return 3 return 0 def split_cfg_ln(ln: str) -> dict[str, Any]: # "a, b, c: 3" => {a:true, b:true, c:3} ret = {} while True: ln = ln.strip() if not ln: break ofs_sep = ln.find(",") + 1 ofs_var = ln.find(":") + 1 if not ofs_sep and not ofs_var: ret[ln] = True break if ofs_sep and (ofs_sep < ofs_var or not ofs_var): k, ln = ln.split(",", 1) ret[k.strip()] = True else: k, ln = ln.split(":", 1) ret[k.strip()] = ln.strip() break return ret def expand_config_file( log: Optional["NamedLogger"], ret: list[str], fp: str, ipath: str ) -> None: """expand all % file includes""" fp = absreal(fp) if len(ipath.split(" -> ")) > 64: raise Exception("hit max depth of 64 includes") if os.path.isdir(fp): names = list(sorted(os.listdir(fp))) cnames = [ x for x in names if x.lower().endswith(".conf") and not x.startswith(".") ] if not cnames: t = "warning: tried to read config-files from folder '%s' but it does not contain any " if names: t += ".conf files; the following files/subfolders were ignored: %s" t = t % (fp, ", ".join(names[:8])) else: t += "files at all" t = t % (fp,) if log: log(t, 3) ret.append("#\033[33m %s\033[0m" % (t,)) else: zs = "#\033[36m cfg files in %s => %s\033[0m" % (fp, cnames) ret.append(zs) for fn in cnames: fp2 = os.path.join(fp, fn) if fp2 in ipath: continue expand_config_file(log, ret, fp2, ipath) return if not os.path.exists(fp): t = "warning: tried to read config from '%s' but the file/folder does not exist" t = t % (fp,) if log: log(t, 3) ret.append("#\033[31m %s\033[0m" % (t,)) return ipath += " -> " + fp ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath)) cfg_lines = read_utf8(log, fp, True).replace("\t", " ").split("\n") if True: # diff-golf for oln in [x.rstrip() for x in cfg_lines]: ln = oln.split(" #")[0].strip() if ln.startswith("% "): pad = " " * len(oln.split("%")[0]) fp2 = ln[1:].strip() fp2 = os.path.join(os.path.dirname(fp), fp2) ofs = len(ret) expand_config_file(log, ret, fp2, ipath) for n in range(ofs, len(ret)): ret[n] = pad + ret[n] continue ret.append(oln) ret.append("#\033[36m closed{}\033[0m".format(ipath)) zsl = [] for ln in ret: zs = ln.split(" #")[0] if " #" in zs and zs.split("#")[0].strip(): zsl.append(ln) if zsl and "no-cfg-cmt-warn" not in "\n".join(ret): t = "\033[33mWARNING: there is less than two spaces before the # in the following config lines, so instead of assuming that this is a comment, the whole line will become part of the config value:\n\n>>> %s\n\nif you are familiar with this and would like to mute this warning, specify the global-option no-cfg-cmt-warn\n\033[0m" t = t % ("\n>>> ".join(zsl),) if log: log(t) else: print(t, file=sys.stderr) def upgrade_cfg_fmt( log: Optional["NamedLogger"], args: argparse.Namespace, orig: list[str], cfg_fp: str ) -> list[str]: """convert from v1 to v2 format""" zst = [x.split("#")[0].strip() for x in orig] zst = [x for x in zst if x] if ( "[global]" in zst or "[accounts]" in zst or "accs:" in zst or "flags:" in zst or [x for x in zst if x.startswith("[/")] or len(zst) == len([x for x in zst if x.startswith("%")]) ): return orig zst = [x for x in orig if "#\033[36m opening cfg file" not in x] incl = len(zst) != len(orig) - 1 t = "upgrading config file [{}] from v1 to v2" if not args.vc: t += ". Run with argument '--vc' to see the converted config if you want to upgrade" if incl: t += ". Please don't include v1 configs from v2 files or vice versa! Upgrade all of them at the same time." if log: log(t.format(cfg_fp), 3) ret = [] vp = "" ap = "" cat = "" catg = "[global]" cata = "[accounts]" catx = " accs:" catf = " flags:" for ln in orig: sn = ln.strip() if not sn: cat = vp = ap = "" if not sn.split("#")[0]: ret.append(ln) elif sn.startswith("-") and cat in ("", catg): if cat != catg: cat = catg ret.append(cat) sn = sn.lstrip("-") zst = sn.split(" ", 1) if len(zst) > 1: sn = "{}: {}".format(zst[0], zst[1].strip()) ret.append(" " + sn) elif sn.startswith("u ") and cat in ("", catg, cata): if cat != cata: cat = cata ret.append(cat) s1, s2 = sn[1:].split(":", 1) ret.append(" {}: {}".format(s1.strip(), s2.strip())) elif not ap: ap = sn elif not vp: vp = "/" + sn.strip("/") cat = "[{}]".format(vp) ret.append(cat) ret.append(" " + ap) elif sn.startswith("c "): if cat != catf: cat = catf ret.append(cat) sn = sn[1:].strip() if "=" in sn: zst = sn.split("=", 1) sn = zst[0].replace(",", ", ") sn += ": " + zst[1] else: sn = sn.replace(",", ", ") ret.append(" " + sn) elif sn[:1] in "rwmdgGhaA.": if cat != catx: cat = catx ret.append(cat) zst = sn.split(" ") zst = [x for x in zst if x] if len(zst) == 1: zst.append("*") ret.append(" {}: {}".format(zst[0], ", ".join(zst[1:]))) else: t = "did not understand line {} in the config" t1 = t n = 0 for ln in orig: n += 1 t += "\n{:4} {}".format(n, ln) if log: log(t, 1) else: print("\033[31m" + t) raise Exception(t1) if args.vc and log: t = "new config syntax (copy/paste this to upgrade your config):\n" t += "\n# ======================[ begin upgraded config ]======================\n\n" for ln in ret: t += ln + "\n" t += "\n# ======================[ end of upgraded config ]======================\n" log(t) return ret ================================================ FILE: copyparty/bos/__init__.py ================================================ ================================================ FILE: copyparty/bos/bos.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os import time from ..util import SYMTIME, fsdec, fsenc from . import path as path if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union from ..util import NamedLogger MKD_755 = {"chmod_d": 0o755} MKD_700 = {"chmod_d": 0o700} UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646)) _ = (path, MKD_755, MKD_700, UTIME_CLAMPS) __all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"] # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" 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, 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) while bname: if os.path.isdir(bname) or bname in todo: break todo.append(bname) bname = os.path.dirname(bname) if not todo: if not exist_ok: os.mkdir(bname) # to throw return False mode = vf["chmod_d"] chown = "chown" in vf for zb in todo[::-1]: try: os.mkdir(zb, mode) if chown: os.chown(zb, vf["uid"], vf["gid"]) except: if os.path.isdir(zb): continue raise return True def mkdir(p: str, mode: int = 0o755) -> None: return os.mkdir(fsenc(p), mode) def open(p: str, *a, **ka) -> int: return os.open(fsenc(p), *a, **ka) def readlink(p: str) -> str: return fsdec(os.readlink(fsenc(p))) def rename(src: str, dst: str) -> None: return os.rename(fsenc(src), fsenc(dst)) def replace(src: str, dst: str) -> None: return os.replace(fsenc(src), fsenc(dst)) def rmdir(p: str) -> None: return os.rmdir(fsenc(p)) def stat(p: str) -> os.stat_result: return os.stat(fsenc(p)) def unlink(p: str) -> None: return os.unlink(fsenc(p)) def utime( p: str, times: Optional[tuple[float, float]] = None, follow_symlinks: bool = True ) -> None: if SYMTIME: return os.utime(fsenc(p), times, follow_symlinks=follow_symlinks) else: return os.utime(fsenc(p), times) def utime_c( log: Union["NamedLogger", Any], p: str, ts: float, follow_symlinks: bool = True, throw: bool = False, ) -> Optional[float]: clamp = 0 ov = ts bp = fsenc(p) now = time.time() while True: try: if SYMTIME: os.utime(bp, (now, ts), follow_symlinks=follow_symlinks) else: os.utime(bp, (now, ts)) if clamp: t = "filesystem rejected utime(%r); clamped %s to %s" log(t % (p, ov, ts)) return ts except Exception as ex: pv = ts while clamp < len(UTIME_CLAMPS): fun, cv = UTIME_CLAMPS[clamp] ts = fun(ts, cv) clamp += 1 if ts != pv: break if clamp >= len(UTIME_CLAMPS): if throw: raise else: t = "could not utime(%r) to %s; %s, %r" log(t % (p, ov, ex, ex)) return None if hasattr(os, "lstat"): def lstat(p: str) -> os.stat_result: return os.lstat(fsenc(p)) else: lstat = stat ================================================ FILE: copyparty/bos/path.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os from ..util import SYMTIME, fsdec, fsenc def abspath(p: str) -> str: return fsdec(os.path.abspath(fsenc(p))) def exists(p: str) -> bool: return os.path.exists(fsenc(p)) def getmtime(p: str, follow_symlinks: bool = True) -> float: if not follow_symlinks and SYMTIME: return os.lstat(fsenc(p)).st_mtime else: return os.path.getmtime(fsenc(p)) def getsize(p: str) -> int: return os.path.getsize(fsenc(p)) def isfile(p: str) -> bool: return os.path.isfile(fsenc(p)) def isdir(p: str) -> bool: return os.path.isdir(fsenc(p)) def islink(p: str) -> bool: return os.path.islink(fsenc(p)) def lexists(p: str) -> bool: return os.path.lexists(fsenc(p)) def realpath(p: str) -> str: return fsdec(os.path.realpath(fsenc(p))) ================================================ FILE: copyparty/broker_mp.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import threading import time import traceback import queue from .__init__ import CORES, TYPE_CHECKING from .broker_mpw import MpWorker from .broker_util import ExceptionalQueue, NotExQueue, try_exec from .util import Daemon, mp if TYPE_CHECKING: from .svchub import SvcHub if True: # pylint: disable=using-constant-test from typing import Any, Union class MProcess(mp.Process): def __init__( self, q_pend: queue.Queue[tuple[int, str, list[Any]]], q_yield: queue.Queue[tuple[int, str, list[Any]]], target: Any, args: Any, ) -> None: super(MProcess, self).__init__(target=target, args=args) self.q_pend = q_pend self.q_yield = q_yield class BrokerMp(object): """external api; manages MpWorkers""" def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.log = hub.log self.args = hub.args self.procs = [] self.mutex = threading.Lock() self.retpend: dict[int, Any] = {} self.retpend_mutex = threading.Lock() self.num_workers = self.args.j or CORES self.log("broker", "booting {} subprocesses".format(self.num_workers)) for n in range(1, self.num_workers + 1): q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1) # type: ignore q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64) # type: ignore proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n)) Daemon(self.collector, "mp-sink-{}".format(n), (proc,)) self.procs.append(proc) proc.start() Daemon(self.periodic, "mp-periodic") def shutdown(self) -> None: self.log("broker", "shutting down") for n, proc in enumerate(self.procs): name = "mp-shut-%d-%d" % (n, len(self.procs)) Daemon(proc.q_pend.put, name, ((0, "shutdown", []),)) with self.mutex: procs = self.procs self.procs = [] while procs: if procs[-1].is_alive(): time.sleep(0.05) continue procs.pop() def reload(self) -> None: self.log("broker", "reloading") for _, proc in enumerate(self.procs): proc.q_pend.put((0, "reload", [])) def reload_sessions(self) -> None: for _, proc in enumerate(self.procs): proc.q_pend.put((0, "reload_sessions", [])) def collector(self, proc: MProcess) -> None: """receive message from hub in other process""" while True: msg = proc.q_yield.get() retq_id, dest, args = msg if dest == "log": self.log(*args) elif dest == "retq": with self.retpend_mutex: retq = self.retpend.pop(retq_id) retq.put(args[0]) else: # new ipc invoking managed service in hub try: obj = self.hub for node in dest.split("."): obj = getattr(obj, node) # TODO will deadlock if dest performs another ipc rv = try_exec(retq_id, obj, *args) except: rv = ["exception", "stack", traceback.format_exc()] if retq_id: proc.q_pend.put((retq_id, "retq", rv)) def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]: # new non-ipc invoking managed service in hub obj = self.hub for node in dest.split("."): obj = getattr(obj, node) rv = try_exec(True, obj, *args) retq = ExceptionalQueue(1) retq.put(rv) return retq def wask(self, dest: str, *args: Any) -> list[Union[ExceptionalQueue, NotExQueue]]: # call from hub to workers ret = [] for p in self.procs: retq = ExceptionalQueue(1) retq_id = id(retq) with self.retpend_mutex: self.retpend[retq_id] = retq p.q_pend.put((retq_id, dest, list(args))) ret.append(retq) return ret def say(self, dest: str, *args: Any) -> None: """ send message to non-hub component in other process, returns a Queue object which eventually contains the response if want_retval (not-impl here since nothing uses it yet) """ if dest == "httpsrv.listen": for p in self.procs: p.q_pend.put((0, dest, [args[0], len(self.procs)])) elif dest == "httpsrv.set_netdevs": for p in self.procs: p.q_pend.put((0, dest, list(args))) elif dest == "cb_httpsrv_up": self.hub.cb_httpsrv_up() elif dest == "httpsrv.set_bad_ver": for p in self.procs: p.q_pend.put((0, dest, list(args))) else: raise Exception("what is " + str(dest)) def periodic(self) -> None: while True: time.sleep(1) tdli = {} tdls = {} qs = self.wask("httpsrv.read_dls") for q in qs: qr = q.get() dli, dls = qr tdli.update(dli) tdls.update(dls) tdl = (tdli, tdls) for p in self.procs: p.q_pend.put((0, "httpsrv.write_dls", tdl)) ================================================ FILE: copyparty/broker_mpw.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import os import signal import sys import threading import queue from .__init__ import ANYWIN from .authsrv import AuthSrv from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue from .fsutil import ramdisk_chk from .httpsrv import HttpSrv from .util import FAKE_MP, Daemon, HMaccas if True: # pylint: disable=using-constant-test from types import FrameType from typing import Any, Optional, Union class MpWorker(BrokerCli): """one single mp instance""" def __init__( self, q_pend: queue.Queue[tuple[int, str, list[Any]]], q_yield: queue.Queue[tuple[int, str, list[Any]]], args: argparse.Namespace, n: int, ) -> None: super(MpWorker, self).__init__() self.q_pend = q_pend self.q_yield = q_yield self.args = args self.n = n self.log = self._log_disabled if args.q and not args.lo else self._log_enabled self.retpend: dict[int, Any] = {} self.retpend_mutex = threading.Lock() self.mutex = threading.Lock() # we inherited signal_handler from parent, # replace it with something harmless if not FAKE_MP: sigs = [signal.SIGINT, signal.SIGTERM] if not ANYWIN: sigs.append(signal.SIGUSR1) for sig in sigs: signal.signal(sig, self.signal_handler) # starting to look like a good idea self.asrv = AuthSrv(args, None, False) ramdisk_chk(self.asrv) # instantiate all services here (TODO: inheritance?) self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8) self.httpsrv = HttpSrv(self, n) # on winxp and some other platforms, # use thr.join() to block all signals Daemon(self.main, "mpw-main").join() def signal_handler(self, sig: Optional[int], frame: Optional[FrameType]) -> None: # print('k') pass def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: self.q_yield.put((0, "log", [src, msg, c])) def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: pass def logw(self, msg: str, c: Union[int, str] = 0) -> None: self.log("mp%d" % (self.n,), msg, c) def main(self) -> None: while True: retq_id, dest, args = self.q_pend.get() if dest == "retq": # response from previous ipc call with self.retpend_mutex: retq = self.retpend.pop(retq_id) retq.put(args) continue if dest == "shutdown": self.httpsrv.shutdown() self.logw("ok bye") sys.exit(0) return if dest == "reload": self.logw("mpw.asrv reloading") self.asrv.reload() ramdisk_chk(self.asrv) self.logw("mpw.asrv reloaded") continue if dest == "reload_sessions": with self.asrv.mutex: self.asrv.load_sessions() continue obj = self for node in dest.split("."): obj = getattr(obj, node) rv = obj(*args) # type: ignore if retq_id: self.say("retq", rv, retq_id=retq_id) def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]: retq = ExceptionalQueue(1) retq_id = id(retq) with self.retpend_mutex: self.retpend[retq_id] = retq self.q_yield.put((retq_id, dest, list(args))) return retq def say(self, dest: str, *args: Any, retq_id=0) -> None: self.q_yield.put((retq_id, dest, list(args))) ================================================ FILE: copyparty/broker_thr.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os import threading from .__init__ import TYPE_CHECKING from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue from .httpsrv import HttpSrv from .util import HMaccas if TYPE_CHECKING: from .svchub import SvcHub if True: # pylint: disable=using-constant-test from typing import Any, Union class BrokerThr(BrokerCli): """external api; behaves like BrokerMP but using plain threads""" def __init__(self, hub: "SvcHub") -> None: super(BrokerThr, self).__init__() self.hub = hub self.log = hub.log self.args = hub.args self.asrv = hub.asrv self.mutex = threading.Lock() self.num_workers = 1 # instantiate all services here (TODO: inheritance?) self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8) self.httpsrv = HttpSrv(self, None) self.reload = self.noop self.reload_sessions = self.noop def shutdown(self) -> None: # self.log("broker", "shutting down") self.httpsrv.shutdown() def noop(self) -> None: pass def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]: # new ipc invoking managed service in hub obj = self.hub for node in dest.split("."): obj = getattr(obj, node) return NotExQueue(obj(*args)) # type: ignore def say(self, dest: str, *args: Any) -> None: if dest.startswith("httpsrv."): if dest == "httpsrv.listen": self.httpsrv.listen(args[0], 1) return if dest == "httpsrv.set_netdevs": self.httpsrv.set_netdevs(args[0]) return if dest == "httpsrv.set_bad_ver": self.httpsrv.set_bad_ver() return # new ipc invoking managed service in hub obj = self.hub for node in dest.split("."): obj = getattr(obj, node) obj(*args) # type: ignore ================================================ FILE: copyparty/broker_util.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse from queue import Queue from .__init__ import TYPE_CHECKING from .authsrv import AuthSrv from .util import HMaccas, Pebkac if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union from .util import RootLogger if TYPE_CHECKING: from .httpsrv import HttpSrv class ExceptionalQueue(Queue, object): def get(self, block: bool = True, timeout: Optional[float] = None) -> Any: rv = super(ExceptionalQueue, self).get(block, timeout) if isinstance(rv, list): if rv[0] == "exception": if rv[1] == "pebkac": raise Pebkac(*rv[2:]) else: raise rv[2] return rv class NotExQueue(object): """ BrokerThr uses this instead of ExceptionalQueue; 7x faster """ def __init__(self, rv: Any) -> None: self.rv = rv def get(self) -> Any: return self.rv class BrokerCli(object): """ helps mypy understand httpsrv.broker but still fails a few levels deeper, for example resolving httpconn.* in httpcli -- see lines tagged #mypy404 """ log: "RootLogger" args: argparse.Namespace asrv: AuthSrv httpsrv: "HttpSrv" iphash: HMaccas def __init__(self) -> None: pass def ask(self, dest: str, *args: Any) -> Union[ExceptionalQueue, NotExQueue]: return ExceptionalQueue(1) def say(self, dest: str, *args: Any) -> None: pass def try_exec(want_retval: Union[bool, int], func: Any, *args: list[Any]) -> Any: try: return func(*args) except Pebkac as ex: if not want_retval: raise return ["exception", "pebkac", ex.code, str(ex)] except Exception as ex: if not want_retval: raise return ["exception", "stack", ex] ================================================ FILE: copyparty/cert.py ================================================ import calendar import errno import json import os import shutil import time from .__init__ import ANYWIN from .util import Netdev, atomic_move, load_resource, runcmd, wunlink HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL") if True: # pylint: disable=using-constant-test from .util import NamedLogger, RootLogger if ANYWIN: VF = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} else: VF = {"mv_re_t": 0, "rm_re_t": 0} def _sp_err(exe, what, rc, so, se, sin): try: zs = shutil.which(exe) except: zs = "" try: zi = os.path.getsize(zs) except: zi = 0 t = "failed to %s; error %s using %s (%s):\n STDOUT: %s\n STDERR: %s\n STDIN: %s\n" raise Exception(t % (what, rc, zs, zi, so, se, sin.decode("utf-8"))) def ensure_cert(log: "RootLogger", args) -> None: """ the default cert (and the entire TLS support) is only here to enable the crypto.subtle javascript API, which is necessary due to the webkit guys being massive memers (https://www.chromium.org/blink/webcrypto) i feel awful about this and so should they """ with load_resource(args.E, "res/insecure.pem") as f: cert_insec = f.read() cert_appdata = os.path.join(args.E.cfg, "cert.pem") if not os.path.isfile(args.cert): if cert_appdata != args.cert: raise Exception("certificate file does not exist: " + args.cert) with open(args.cert, "wb") as f: f.write(cert_insec) with open(args.cert, "rb") as f: buf = f.read() o1 = buf.find(b" PRIVATE KEY-") o2 = buf.find(b" CERTIFICATE-") m = "unsupported certificate format: " if o1 < 0: raise Exception(m + "no private key inside pem") if o2 < 0: raise Exception(m + "no server certificate inside pem") if o1 > o2: raise Exception(m + "private key must appear before server certificate") try: with open(args.cert, "rb") as f: active_cert = f.read() if active_cert == cert_insec: t = "using default TLS certificate; https will be insecure:\033[36m {}" log("cert", t.format(args.cert), 3) except: pass # speaking of the default cert, # printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout def _read_crt(args, fn): try: if not os.path.exists(os.path.join(args.crt_dir, fn)): return 0, {} acmd = ["cfssl-certinfo", "-cert", fn] rc, so, se = runcmd(acmd, cwd=args.crt_dir) if rc: return 0, {} inf = json.loads(so) zs = inf["not_after"] expiry = calendar.timegm(time.strptime(zs, "%Y-%m-%dT%H:%M:%SZ")) return expiry, inf except OSError as ex: if ex.errno == errno.ENOENT: raise return 0, {} except: return 0, {} def _gen_ca(log: "RootLogger", args): nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-ca", msg, c) expiry = _read_crt(args, "ca.pem")[0] if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry: return backdate = "{}m".format(int(args.crt_back * 60)) expiry = "{}m".format(int(args.crt_cdays * 60 * 24)) cn = args.crt_cnc.replace("--crt-cn", args.crt_cn) algo, ksz = args.crt_alg.split("-") req = { "CN": cn, "CA": {"backdate": backdate, "expiry": expiry, "pathlen": 0}, "key": {"algo": algo, "size": int(ksz)}, "names": [{"O": cn}], } sin = json.dumps(req).encode("utf-8") log("cert", "creating new ca ...", 6) cmd = "cfssl gencert -initca -" rc, so, se = runcmd(cmd.split(), 30, sin=sin) if rc: _sp_err("cfssl", "create ca-cert", rc, so, se, sin) cmd = "cfssljson -bare ca" sin = so.encode("utf-8") rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir) if rc: _sp_err("cfssljson", "translate ca-cert", rc, so, se, sin) bname = os.path.join(args.crt_dir, "ca") try: wunlink(nlog, bname + ".key", VF) except: pass atomic_move(nlog, bname + "-key.pem", bname + ".key", VF) wunlink(nlog, bname + ".csr", VF) log("cert", "new ca OK", 2) def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]): nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c) names = args.crt_ns.split(",") if args.crt_ns else [] names = [x.strip() for x in names] if not args.crt_exact: for n in names[:]: names.append("*.{}".format(n)) if not args.crt_noip: for ip in netdevs.keys(): names.append(ip.split("/")[0]) if args.crt_nolo: names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")] if not args.crt_nohn: names.append(args.name) names.append(args.name + ".local") if not names: names = ["127.0.0.1"] if "127.0.0.1" in names or "::1" in names: names.append("localhost") names = list({x: 1 for x in names}.keys()) try: expiry, inf = _read_crt(args, "srv.pem") if "sans" not in inf: raise Exception("no useable cert found") expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > expiry if expired: raise Exception("old server-cert has expired") for n in names: if n not in inf["sans"]: raise Exception("does not have {}".format(n)) with load_resource(args.E, "res/insecure.pem") as f: cert_insec = f.read() with open(args.cert, "rb") as f: active_cert = f.read() if active_cert and active_cert != cert_insec: return except Exception as ex: log("cert", "will create new server-cert; {}".format(ex)) log("cert", "creating server-cert ...", 6) backdate = "{}m".format(int(args.crt_back * 60)) expiry = "{}m".format(int(args.crt_sdays * 60 * 24)) cfg = { "signing": { "default": { "backdate": backdate, "expiry": expiry, "usages": ["signing", "key encipherment", "server auth"], } } } with open(os.path.join(args.crt_dir, "cfssl.json"), "wb") as f: f.write(json.dumps(cfg).encode("utf-8")) cn = args.crt_cns.replace("--crt-cn", args.crt_cn) algo, ksz = args.crt_alg.split("-") req = { "key": {"algo": algo, "size": int(ksz)}, "names": [{"O": cn}], } sin = json.dumps(req).encode("utf-8") cmd = "cfssl gencert -config=cfssl.json -ca ca.pem -ca-key ca.key -profile=www" acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"] rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir) if rc: _sp_err("cfssl", "create cert", rc, so, se, sin) cmd = "cfssljson -bare srv" sin = so.encode("utf-8") rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir) if rc: _sp_err("cfssljson", "translate cert", rc, so, se, sin) bname = os.path.join(args.crt_dir, "srv") try: wunlink(nlog, bname + ".key", VF) except: pass atomic_move(nlog, bname + "-key.pem", bname + ".key", VF) wunlink(nlog, bname + ".csr", VF) with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f: ca = f.read() with open(bname + ".key", "rb") as f: skey = f.read() with open(bname + ".pem", "rb") as f: scrt = f.read() with open(args.cert, "wb") as f: f.write(skey + scrt + ca) log("cert", "new server-cert OK", 2) def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]): global HAVE_CFSSL if args.http_only: return if args.no_crt or not HAVE_CFSSL: ensure_cert(log, args) return try: _gen_ca(log, args) _gen_srv(log, args, netdevs) except Exception as ex: HAVE_CFSSL = False log("cert", "could not create TLS certificates: {}".format(ex), 3) if getattr(ex, "errno", 0) == errno.ENOENT: t = "install cfssl if you want to fix this; https://github.com/cloudflare/cfssl/releases/latest (cfssl, cfssljson, cfssl-certinfo)" log("cert", t, 6) ensure_cert(log, args) ================================================ FILE: copyparty/cfg.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals # awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' ' zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nth nw p q s ss sss v z zv" onedash = set(zs.split()) # verify that all volflags are documented here: # grep volflag= __main__.py | sed -r 's/.*volflag=//;s/\).*//' | sort | uniq | while IFS= read -r x; do grep -E "\"$x(=[^ \"]+)?\": \"" cfg.py || printf '%s\n' "$x"; done def vf_bmap() -> dict[str, str]: """argv-to-volflag: simple bools""" ret = { "dav_auth": "davauth", "dav_rt": "davrt", "ed": "dots", "hardlink_only": "hardlinkonly", "no_clone": "noclone", "no_dirsz": "nodirsz", "no_dupe": "nodupe", "no_dupe_m": "nodupem", "no_forget": "noforget", "no_pipe": "nopipe", "no_robots": "norobots", "no_tail": "notail", "no_thumb": "dthumb", "no_vthumb": "dvthumb", "no_athumb": "dathumb", "vol_nospawn": "nospawn", "vol_or_crash": "assert_root", } for k in ( "dedup", "dlni", "dotsrch", "e2d", "e2ds", "e2dsa", "e2t", "e2ts", "e2tsr", "e2v", "e2vu", "e2vp", "exp", "grid", "gsel", "hardlink", "magic", "md_no_br", "no_db_ip", "no_sb_md", "no_sb_lg", "nsort", "og", "og_no_head", "og_s_title", "opds", "rand", "reflink", "rm_partial", "rmagic", "rss", "ui_noacci", "ui_nocpla", "ui_nolbar", "ui_nombar", "ui_nonav", "ui_notree", "ui_norepl", "ui_nosrvi", "ui_noctxb", "wo_up_readme", "wram", "xdev", "xlink", "xvol", "zipmaxu", ): ret[k] = k return ret def vf_vmap() -> dict[str, str]: """argv-to-volflag: simple values""" ret = { "ac_convt": "aconvt", "no_hash": "nohash", "no_idx": "noidx", "re_maxage": "scan", "safe_dedup": "safededup", "th_convt": "convt", "th_size": "thsize", "th_crop": "crop", "th_x3": "th3x", } for k in ( "apnd_who", "bup_ck", "cachectl", "casechk", "chmod_d", "chmod_f", "dbd", "db_xattr", "du_who", "epilogues", "ufavico", "forget_ip", "fsnt", "hsortn", "html_head", "html_head_s", "lg_sbf", "md_sbf", "lg_sba", "md_sba", "md_hist", "nrand", "u2ow", "og_desc", "og_site", "og_th", "og_title", "og_title_a", "og_title_v", "og_title_i", "og_tpl", "og_ua", "opds_exts", "prologues", "preadmes", "put_ck", "put_name", "readmes", "mv_retry", "rm_retry", "rss_sort", "rss_fmt_t", "rss_fmt_d", "rw_edit", "shr_who", "sort", "tail_fd", "tail_rate", "tail_tmax", "tail_who", "tcolor", "th_qv", "th_qvx", "th_spec_p", "txt_eol", "unlist", "u2abort", "u2ts", "uid", "gid", "unp_who", "ups_who", "zip_who", "zipmaxn", "zipmaxs", "zipmaxt", ): ret[k] = k return ret def vf_cmap() -> dict[str, str]: """argv-to-volflag: complex/lists""" ret = {} for k in ( "exp_lg", "exp_md", "ext_th", "mte", "mth", "mtp", "xac", "xad", "xar", "xau", "xban", "xbc", "xbd", "xbr", "xbu", "xiu", "xm", ): ret[k] = k return ret permdescs = { "r": "read; list folder contents, download files", "w": 'write; upload files; need "r" to see the uploads', "m": 'move; move files and folders; need "w" at destination', "d": "delete; permanently delete files and folders", ".": "dots; user can ask to show dotfiles in listings", "g": "get; download files, but cannot see folder contents", "G": 'upget; same as "g" but can see filekeys of their own uploads', "h": 'html; same as "g" but folders return their index.html', "a": "admin; can see uploader IPs, config-reload", "A": "all; same as 'rwmda.' (read/write/move/delete/dotfiles)", } flagcats = { "uploads, general": { "dedup": "enable symlink-based file deduplication", "hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible", "hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible", "reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible", "safededup": "verify on-disk data before using it for dedup", "noclone": "take dupe data from clients, even if available on HDD", "nodupe": "rejects existing files (instead of linking/cloning them)", "nodupem": "rejects existing files during moves as well", "casechk=auto": "actively prevent case-insensitive filesystem? y/n", "chmod_d=755": "unix-permission for new dirs/folders", "chmod_f=644": "unix-permission for new files", "uid=573": "change owner of new files/folders to unix-user 573", "gid=999": "change owner of new files/folders to unix-group 999", "fsnt=auto": "filesystem filename traits (lin/win/mac/auto)", "wram": "allow uploading into ramdisks", "sparse": "force use of sparse files, mainly for s3-backed storage", "nosparse": "deny use of sparse files, mainly for slow storage", "rm_partial": "delete unfinished uploads from HDD when they timeout", "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", "nosub": "forces all uploads into the top folder of the vfs", "magic": "enables filetype detection for nameless uploads", "put_name": "fallback filename for nameless uploads", "put_ck": "default checksum-hasher for PUT/WebDAV uploads", "bup_ck": "default checksum-hasher for bup/basic uploads", "gz": "allows server-side gzip compression of uploads with ?gz", "xz": "allows server-side lzma compression of uploads with ?xz", "pk": "forces server-side compression, optional arg: xz,9", }, "upload rules": { "apnd_who=dw": "who can append? (aw/dw/w/no)", "maxn=250,600": "max 250 uploads over 15min", "maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)", "vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)", "vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)", "medialinks": "return medialinks for non-up2k uploads (not hotlinks)", "wo_up_readme": "write-only users can upload logues without getting renamed", "rand": "force randomized filenames, 9 chars long by default", "nrand=N": "randomized filenames are N chars long", "u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always", "u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time", "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk", "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB", "df=1g": "ensure 1 GiB free disk space", }, "upload rotation\n(moves all uploads into the specified folder structure)": { "rotn=100,3": "3 levels of subfolders with 100 entries in each", "rotf=%Y-%m/%d-%H": "date-formatted organizing", "rotf_tz=Europe/Oslo": "timezone (default=UTC)", "lifetime=3600": "uploads are deleted after 1 hour", }, "database, general": { "e2d": "enable database; makes files searchable + enables upload-undo", "e2ds": "scan writable folders for new files on startup; also sets -e2d", "e2dsa": "scans all folders for new files on startup; also sets -e2d", "e2t": "enable multimedia indexing; makes it possible to search for tags", "e2ts": "scan existing files for tags on startup; also sets -e2t", "e2tsr": "delete all metadata from DB (full rescan); also sets -e2ts", "d2ts": "disables metadata collection for existing files", "e2v": "verify integrity on startup by hashing files and comparing to db", "e2vu": "when e2v fails, update the db (assume on-disk files are good)", "e2vp": "when e2v fails, panic and quit copyparty", "d2ds": "disables onboot indexing, overrides -e2ds*", "d2t": "disables metadata collection, overrides -e2t*", "d2v": "disables file verification, overrides -e2v*", "d2d": "disables all database stuff, overrides -e2*", "hist=/tmp/cdb": "puts thumbnails and indexes at that location", "dbpath=/tmp/cdb": "puts indexes at that location", "landmark=foo": "disable db if file foo doesn't exist", "scan=60": "scan for new files every 60sec, same as --re-maxage", "nohash=\\.iso$": "skips hashing file contents if path matches *.iso", "noidx=\\.iso$": "fully ignores the contents at paths matching *.iso", "noforget": "don't forget files when deleted from disk", "forget_ip=43200": "forget uploader-IP after 30 days (GDPR)", "no_db_ip": "never store uploader-IP in the db; disables unpost", "fat32": "avoid excessive reindexing on android sdcardfs", "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff", "xlink": "cross-volume dupe detection / linking (dangerous)", "xdev": "do not descend into other filesystems", "xvol": "do not follow symlinks leaving the volume root", "dotsrch": "show dotfiles in search results", "nodotsrch": "hide dotfiles in search results (default)", "srch_excl": "exclude search results with URL matching this regex", "db_xattr=user.foo,user.bar": "index file xattrs as media-tags", }, 'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': { "mte=artist,title": "media-tags to index/display", "mth=fmt,res,ac": "media-tags to hide by default", "mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)', "mtp=ahash,vhash=media-hash.py": "collects two tags at once", }, "thumbnails": { "dthumb": "disables all thumbnails", "dvthumb": "disables video thumbnails", "dathumb": "disables audio thumbnails (spectrograms)", "dithumb": "disables image thumbnails", "pngquant": "compress audio waveforms 33% better", "thsize": "thumbnail res; WxH", "crop": "center-cropping (y/n/fy/fn)", "th3x": "3x resolution (y/n/fy/fn)", "th_qv=40": "webp/jpg thumbnail quality (10~90)", "th_qvx=40": "jxl thumbnail quality (10~90)", "convt": "convert-to-image timeout in seconds", "aconvt": "convert-to-audio timeout in seconds", "th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always", "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s", }, "handlers\n(better explained in --help-handlers)": { "on404=PY": "handle 404s by executing PY file", "on403=PY": "handle 403s by executing PY file", }, "event hooks\n(better explained in --help-hooks)": { "xbu=CMD": "execute CMD before a file upload starts", "xau=CMD": "execute CMD after a file upload finishes", "xiu=CMD": "execute CMD after all uploads finish and volume is idle", "xbc=CMD": "execute CMD before a file copy", "xac=CMD": "execute CMD after a file copy", "xbr=CMD": "execute CMD before a file rename/move", "xar=CMD": "execute CMD after a file rename/move", "xbd=CMD": "execute CMD before a file delete", "xad=CMD": "execute CMD after a file delete", "xm=CMD": "execute CMD on message", "xban=CMD": "execute CMD if someone gets banned", }, "client and ux": { "grid": "show grid/thumbnails by default", "gsel": "select files in grid by ctrl-click", "sort": "default sort order", "nsort": "natural-sort of leading digits in filenames", "hsortn": "number of sort-rules to add to media URLs", "ufavico=URL": "per-volume favicon (.ico/png/gif/svg)", "unlist": "dont list files matching REGEX", "dlni": "force-download (no-inline) files on click", "html_head=TXT": "includes TXT in the , or @PATH for file at PATH", "html_head_s=TXT": "additional static text in the html ", "tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)", "nodirsz": "don't show total folder size", "du_who=all": "show disk-usage info to everyone", "robots": "allows indexing by search engines (default)", "norobots": "kindly asks search engines to leave", "unlistcr": "don't list read-access in controlpanel", "unlistcw": "don't list write-access in controlpanel", "prologues=.prologue.html": "files to embed above/before files", "epilogues=.epilogue.html": "files to embed below/after files", "readmes=readme.md,README.md": "files to embed as readmes", "preadmes=preadme.md,PREADME.md": "files to embed as preadmes", "no_sb_md": "disable js sandbox for markdown files", "no_sb_lg": "disable js sandbox for prologue/epilogue", "sb_md": "enable js sandbox for markdown files (default)", "sb_lg": "enable js sandbox for prologue/epilogue (default)", "md_sbf": "list of markdown-sandbox safeguards to disable", "lg_sbf": "list of *logue-sandbox safeguards to disable", "md_sba": "value of iframe allow-prop for markdown-sandbox", "lg_sba": "value of iframe allow-prop for *logue-sandbox", "nohtml": "return html and markdown as text/html", "noscript": "disable most javascript by use of CSP", "ui_noacci": "hide account-info in the UI", "ui_nocpla": "hide cpanel-link in the UI", "ui_nolbar": "hide link-bar in the UI", "ui_nombar": "hide top-menu in the UI", "ui_nonav": "hide navpane+breadcrumbs in the UI", "ui_notree": "hide navpane in the UI", "ui_norepl": "hide repl-button in the UI", "ui_nosrvi": "hide server-info in the UI", "ui_noctxb": "hide context-buttons in the UI", }, "opengraph (discord embeds)": { "og": "enable OG (disables hotlinking)", "og_site": "sitename; defaults to --name, disable with '-'", "og_desc": "description text for all files; disable with '-'", "og_th=jf": "thumbnail format; j / jf / jf3 / w / w3 / ...", "og_title_a": "audio title format; default: {{ artist }} - {{ title }}", "og_title_v": "video title format; default: {{ title }}", "og_title_i": "image title format; default: {{ title }}", "og_title=foo": "fallback title if there's nothing in the db", "og_s_title": "force default title; do not read from tags", "og_tpl": "custom html; see --og-tpl in --help", "og_no_head": "you want to add tags manually with og_tpl", "og_ua": "if defined: only send OG html if useragent matches this regex", }, "opds": { "opds": "enable OPDS", "opds_exts": "file formats to list in OPDS feeds; leave empty to show everything", }, "textfiles": { "rw_edit=md,txt": "only require read+write to edit .md and .txt", "md_no_br": "newline only on double-newline or two tailing spaces", "md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope", "exp": "enable textfile expansion; see --help-exp", "exp_md": "placeholders to expand in markdown files; see --help", "exp_lg": "placeholders to expand in prologue/epilogue; see --help", "txt_eol=lf": "enable EOL conversion when writing docs (LF or CRLF)", }, "tailing": { "notail": "disable ?tail (download a growing file continuously)", "tail_fd=1": "check if file was replaced (new fd) every 1 sec", "tail_rate=0.2": "check for new data every 0.2 sec", "tail_tmax=30": "kill connection after 30 sec", "tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)", }, "rss": { "rss": "allow '?rss' URL suffix (experimental)", "rss_sort=m": "default sort-order (m/u/n/s)", "rss_fmt_t={fname}": "default title-format", "rss_fmt_d={album},{.tn}": "default description-format", }, "others": { "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings", "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', "fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers', "dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', "dks": "per-directory accesskeys allow browsing into subdirs", "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', "rmagic": "expensive analysis for mimetype accuracy", "shr_who=auth": "who can create shares? no/auth/a", "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", "ups_who=2": "restrict viewing the list of recent uploads", "zip_who=2": "restrict access to download-as-zip/tar", "zipmaxn=9k": "reject download-as-zip if more than 9000 files", "zipmaxs=2g": "reject download-as-zip if size over 2 GiB", "zipmaxt=no": "reply with 'no' if download-as-zip exceeds max", "zipmaxu": "zip-size-limit does not apply to authenticated users", "nopipe": "disable race-the-beam (download unfinished uploads)", "cachectl=no-cache": "controls caching in webbrowsers", "mv_retry": "ms-windows: timeout for renaming busy files", "rm_retry": "ms-windows: timeout for deleting busy files", "nospawn": "don't create volume's folder if not exist", "assert_root": "crash on startup if volume's folder not exist", "davauth": "ask webdav clients to login for all folders", "davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)", }, } flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()} if True: # so it gets removed in release-builds for fun in [vf_bmap, vf_cmap, vf_vmap]: for k in fun().values(): if k not in flagdescs: raise Exception("undocumented volflag: " + k) ================================================ FILE: copyparty/dxml.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import importlib import sys import xml.etree.ElementTree as ET from .__init__ import PY2 if True: # pylint: disable=using-constant-test from typing import Any, Optional class BadXML(Exception): pass def get_ET() -> ET.XMLParser: pn = "xml.etree.ElementTree" cn = "_elementtree" cmod = sys.modules.pop(cn, None) if not cmod: return ET.XMLParser # type: ignore pmod = sys.modules.pop(pn) sys.modules[cn] = None # type: ignore ret = importlib.import_module(pn) for name, mod in ((pn, pmod), (cn, cmod)): if mod: sys.modules[name] = mod else: sys.modules.pop(name, None) sys.modules["xml.etree"].ElementTree = pmod # type: ignore ret.ParseError = ET.ParseError # type: ignore return ret.XMLParser # type: ignore XMLParser: ET.XMLParser = get_ET() class _DXMLParser(XMLParser): # type: ignore def __init__(self) -> None: tb = ET.TreeBuilder() super(DXMLParser, self).__init__(target=tb) p = self._parser if PY2 else self.parser p.StartDoctypeDeclHandler = self.nope p.EntityDeclHandler = self.nope p.UnparsedEntityDeclHandler = self.nope p.ExternalEntityRefHandler = self.nope def nope(self, *a: Any, **ka: Any) -> None: raise BadXML("{}, {}".format(a, ka)) class _NG(XMLParser): # type: ignore def __int__(self) -> None: raise BadXML("dxml selftest failed") DXMLParser = _DXMLParser def parse_xml(txt: str) -> ET.Element: """ Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts. """ parser = DXMLParser() parser.feed(txt) return parser.close() # type: ignore def selftest() -> bool: qbe = r""" ]> &a;&a;&a;""" emb = r""" ]> &a;""" # future-proofing; there's never been any known vulns # regarding DTDs and ET.XMLParser, but might as well # block them since webdav-clients don't use them dtd = r""" a""" for txt in (qbe, emb, dtd): try: parse_xml(txt) t = "WARNING: dxml selftest failed:\n%s\n" print(t % (txt,), file=sys.stderr) return False except BadXML: pass return True DXML_OK = selftest() if not DXML_OK: DXMLParser = _NG def mktnod(name: str, text: str) -> ET.Element: el = ET.Element(name) el.text = text return el def mkenod(name: str, sub_el: Optional[ET.Element] = None) -> ET.Element: el = ET.Element(name) if sub_el is not None: el.append(sub_el) return el ================================================ FILE: copyparty/fsutil.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import json import os import re import time from .__init__ import ANYWIN, MACOS from .authsrv import AXS, VFS, AuthSrv from .bos import bos from .util import chkcmd, json_hesc, min_ex, undot if True: # pylint: disable=using-constant-test from typing import Optional, Union from .util import RootLogger, undot class Fstab(object): def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool): self.log_func = log self.verbose = verbose self.warned = False self.trusted = False self.tab: Optional[VFS] = None self.oldtab: Optional[VFS] = None self.srctab = "a" self.cache: dict[str, tuple[str, str]] = {} self.age = 0.0 self.maxage = args.mtab_age def log(self, msg: str, c: Union[int, str] = 0) -> None: if not c or self.verbose: return self.log_func("fstab", msg, c) def get(self, path: str) -> tuple[str, str]: now = time.time() if now - self.age > self.maxage or len(self.cache) > 9000: self.age = now self.oldtab = self.tab or self.oldtab self.tab = None self.cache = {} mp = "" fs = "ext4" msg = "failed to determine filesystem at %r; assuming %s\n%s" if ANYWIN: fs = "vfat" try: path = self._winpath(path) except: self.log(msg % (path, fs, min_ex()), 3) return fs, "" path = undot(path) try: return self.cache[path] except: pass try: fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path) except: self.log(msg % (path, fs, min_ex()), 3) fs = fs.lower() self.cache[path] = (fs, mp) self.log("found %s at %r, %r" % (fs, mp, path)) return fs, mp def _winpath(self, path: str) -> str: # try to combine volume-label + st_dev (vsn) path = path.replace("/", "\\") vid = path.split(":", 1)[0].strip("\\").split("\\", 1)[0] try: return "{}*{}".format(vid, bos.stat(path).st_dev) except: return vid def build_fallback(self) -> None: self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {}) self.trusted = False def _from_sp_mount(self) -> dict[str, str]: sptn = r"^.*? on (.*) type ([^ ]+) \(.*" if MACOS: sptn = r"^.*? on (.*) \(([^ ]+), .*" ptn = re.compile(sptn) so, _ = chkcmd(["mount"]) dtab: dict[str, str] = {} for ln in so.split("\n"): m = ptn.match(ln) if not m: continue zs1, zs2 = m.groups() dtab[str(zs1)] = str(zs2) return dtab def _from_proc(self) -> dict[str, str]: ret: dict[str, str] = {} with open("/proc/self/mounts", "rb", 262144) as f: src = f.read(262144).decode("utf-8", "replace").split("\n") for zsl in [x.split(" ") for x in src]: if len(zsl) < 3: continue zs = zsl[1] zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\") ret[zs] = zsl[2] return ret def build_tab(self) -> None: self.log("inspecting mtab for changes") dtab = self._from_sp_mount() if MACOS else self._from_proc() # keep empirically-correct values if mounttab unchanged srctab = str(sorted(dtab.items())) if srctab == self.srctab: self.tab = self.oldtab return self.log("mtab has changed; reevaluating support for sparse files") try: fuses = [mp for mp, fs in dtab.items() if fs == "fuseblk"] if not fuses or MACOS: raise Exception() try: so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINT"]) # centos6 except: so, _ = chkcmd(["lsblk", "-nrfo", "FSTYPE,MOUNTPOINTS"]) # future for ln in so.split("\n"): zsl = ln.split(" ", 1) if len(zsl) != 2: continue fs, mp = zsl if mp in fuses: dtab[mp] = fs except: pass tab1 = list(dtab.items()) tab1.sort(key=lambda x: (len(x[0]), x[0])) path1, fs1 = tab1[0] tab = VFS(self.log_func, fs1, path1, path1, AXS(), {}) for path, fs in tab1[1:]: zs = path.lstrip("/") tab.add(fs, zs, zs) self.tab = tab self.srctab = srctab def relabel(self, path: str, nval: str) -> None: assert self.tab # !rm self.cache = {} if ANYWIN: path = self._winpath(path) path = undot(path) ptn = re.compile(r"^[^\\/]*") vn, rem = self.tab._find(path) if not self.trusted: # no mtab access; have to build as we go if "/" in rem: zs = os.path.join(vn.vpath, rem.split("/")[0]) self.tab.add("idk", zs, zs) if rem: self.tab.add(nval, path, path) else: vn.realpath = nval return visit = [vn] while visit: vn = visit.pop() vn.realpath = ptn.sub(nval, vn.realpath) visit.extend(list(vn.nodes.values())) def get_unix(self, path: str) -> tuple[str, str]: if not self.tab: try: self.build_tab() self.trusted = True except: # prisonparty or other restrictive environment if not self.warned: self.warned = True t = "failed to associate fs-mounts with the VFS (this is fine):\n%s" self.log(t % (min_ex(),), 6) self.build_fallback() assert self.tab # !rm ret = self.tab._find(path)[0] if self.trusted or path == ret.vpath: return ret.realpath.split("/")[0], ret.vpath else: return "idk", "" def get_w32(self, path: str) -> tuple[str, str]: if not self.tab: self.build_fallback() assert self.tab # !rm ret = self.tab._find(path)[0] return ret.realpath, "" _fstab: Optional[Fstab] = None winfs = set(("msdos", "vfat", "ntfs", "exfat")) # "msdos" = vfat on macos def ramdisk_chk(asrv: AuthSrv) -> None: # should have been in authsrv but that's a circular import global _fstab mods = [] ramfs = ("tmpfs", "overlay") log = asrv.log_func or print if not _fstab: _fstab = Fstab(log, asrv.args, False) for vn in asrv.vfs.all_nodes.values(): if not vn.axs.uwrite or "wram" in vn.flags: continue ap = vn.realpath if not ap or os.path.isfile(ap): continue fs, mp = _fstab.get(ap) mp = "/" + mp.strip("/") if fs == "tmpfs" or (mp == "/" and fs in ramfs): mods.append((vn.vpath, ap, fs, mp)) vn.axs.uwrite.clear() vn.axs.umove.clear() for un, ztsp in list(vn.uaxs.items()): zsl = list(ztsp) zsl[1] = False zsl[2] = False vn.uaxs[un] = tuple(zsl) # type: ignore if mods: t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:" t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods] log("vfs", t + "".join(t2) + "\n", 1) assume = "mac" if MACOS else "lin" for vol in asrv.vfs.all_nodes.values(): if not vol.realpath or vol.flags.get("is_file"): continue zs = vol.flags["fsnt"].strip()[:3].lower() if ANYWIN and not zs: zs = "win" if zs in ("lin", "win", "mac"): vol.flags["fsnt"] = zs continue fs = _fstab.get(vol.realpath)[0] fs = "win" if fs in winfs else assume htm = json.loads(vol.js_htm) vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs vol.js_htm = json_hesc(json.dumps(htm)) ================================================ FILE: copyparty/ftpd.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import errno import logging import os import stat import sys import time from pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer from pyftpdlib.filesystems import AbstractedFS, FilesystemError from pyftpdlib.handlers import FTPHandler from pyftpdlib.ioloop import IOLoop from pyftpdlib.servers import FTPServer from .__init__ import PY2, TYPE_CHECKING from .authsrv import VFS from .bos import bos from .util import ( VF_CAREFUL, Daemon, ODict, Pebkac, exclude_dotfiles, fsenc, ipnorm, pybin, relchk, runhook, sanitize_fn, set_fperms, vjoin, wunlink, ) if TYPE_CHECKING: from .svchub import SvcHub if True: # pylint: disable=using-constant-test import typing from typing import Any, Optional, Union if PY2: range = xrange # type: ignore class FSE(FilesystemError): def __init__(self, msg: str, severity: int = 0) -> None: super(FilesystemError, self).__init__(msg) self.severity = severity class FtpAuth(DummyAuthorizer): def __init__(self, hub: "SvcHub") -> None: super(FtpAuth, self).__init__() self.hub = hub def validate_authentication( self, username: str, password: str, handler: Any ) -> None: handler.username = "{}:{}".format(username, password) handler.uname = "*" ip = handler.addr[0] if ip.startswith("::ffff:"): ip = ip[7:] ipn = ipnorm(ip) bans = self.hub.bans if ipn in bans: rt = bans[ipn] - time.time() if rt < 0: logging.info("client unbanned") del bans[ipn] else: raise AuthenticationFailed("banned") args = self.hub.args asrv = self.hub.asrv uname = "*" if username != "anonymous": uname = "" if args.usernames: alts = ["%s:%s" % (username, password)] else: alts = [password, username] for zs in alts: zs = asrv.iacct.get(asrv.ah.hash(zs), "") if zs: uname = zs break if args.ipu and uname == "*": uname = args.ipu_iu[args.ipu_nm.map(ip)] if args.ipr and uname in args.ipr_u: if not args.ipr_u[uname].map(ip): logging.warning("username [%s] rejected by --ipr", uname) uname = "*" if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): g = self.hub.gpwd if g.lim: bonk, ip = g.bonk(ip, handler.username) if bonk: logging.warning("client banned: invalid passwords") bans[ip] = bonk try: # only possible if multiprocessing disabled self.hub.broker.httpsrv.bans[ip] = bonk # type: ignore self.hub.broker.httpsrv.nban += 1 # type: ignore except: pass raise AuthenticationFailed("Authentication failed.") handler.uname = handler.username = uname def get_home_dir(self, username: str) -> str: return "/" def has_user(self, username: str) -> bool: asrv = self.hub.asrv return username in asrv.acct or username in asrv.iacct def has_perm(self, username: str, perm: int, path: Optional[str] = None) -> bool: return True # handled at filesystem layer def get_perms(self, username: str) -> str: return "elradfmwMT" def get_msg_login(self, username: str) -> str: return "sup {}".format(username) def get_msg_quit(self, username: str) -> str: return "cya" class FtpFs(AbstractedFS): def __init__( self, root: str, cmd_channel: Any ) -> None: # pylint: disable=super-init-not-called self.h = cmd_channel # type: FTPHandler self.cmd_channel = cmd_channel # type: FTPHandler self.hub: "SvcHub" = cmd_channel.hub self.args = cmd_channel.args self.uname = cmd_channel.uname self.cwd = "/" # pyftpdlib convention of leading slash self.root = "/var/lib/empty" self.listdirinfo = self.listdir self.chdir(".") def log(self, msg: str, c: Union[int, str] = 0) -> None: self.hub.log("ftpd", msg, c) def v2a( self, vpath: str, r: bool = False, w: bool = False, m: bool = False, d: bool = False, ) -> tuple[str, VFS, str]: try: vpath = vpath.replace("\\", "/").strip("/") rd, fn = os.path.split(vpath) if relchk(rd): logging.warning("malicious vpath: %s", vpath) t = "Unsupported characters in [{}]" raise FSE(t.format(vpath), 1) fn = sanitize_fn(fn or "") vpath = vjoin(rd, fn) vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) if ( w and fn.lower() in vfs.flags["emb_all"] and self.h.uname not in vfs.axs.uread and "wo_up_readme" not in vfs.flags ): fn = "_wo_" + fn vpath = vjoin(rd, fn) vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) if not vfs.realpath: t = "No filesystem mounted at [{}]" raise FSE(t.format(vpath)) if "xdev" in vfs.flags or "xvol" in vfs.flags: ap = vfs.canonical(rem) avfs = vfs.chk_ap(ap) t = "Permission denied in [{}]" if not avfs: raise FSE(t.format(vpath), 1) cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[self.h.uname] if r and not cr or w and not cw or m and not cm or d and not cd: raise FSE(t.format(vpath), 1) else: ap = vfs.canonical(rem, False) if "bcasechk" in vfs.flags and not vfs.casechk(rem, True): raise FSE("No such file or directory", 1) return ap, vfs, rem except Pebkac as ex: raise FSE(str(ex)) def rv2a( self, vpath: str, r: bool = False, w: bool = False, m: bool = False, d: bool = False, ) -> tuple[str, VFS, str]: return self.v2a(join(self.cwd, vpath), r, w, m, d) def ftp2fs(self, ftppath: str) -> str: # return self.v2a(ftppath) return ftppath # self.cwd must be vpath def fs2ftp(self, fspath: str) -> str: # raise NotImplementedError() return fspath def validpath(self, path: str) -> bool: if "/.hist/" in path: if "/up2k." in path or path.endswith("/dir.txt"): raise FSE("Access to this file is forbidden", 1) return True def open(self, filename: str, mode: str) -> typing.IO[Any]: r = "r" in mode w = "w" in mode or "a" in mode or "+" in mode ap, vfs, _ = self.rv2a(filename, r, w) self.validpath(ap) if w: try: st = bos.stat(ap) td = time.time() - st.st_mtime need_unlink = True except: need_unlink = False td = 0 xbu = vfs.flags.get("xbu") if xbu: hr = runhook( self.log, None, self.hub.up2k, "xbu.ftp", xbu, ap, filename, "", "", "", 0, 0, "1.3.8.7", time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (filename,) self.log(t, 3) raise FSE(t) if w and need_unlink: # type: ignore # !rm assert td # type: ignore # !rm if td >= -1 and td <= self.args.ftp_wt: # within permitted timeframe; allow overwrite or resume do_it = True elif self.args.no_del or self.args.ftp_no_ow: # file too old, or overwrite not allowed; reject do_it = False else: # allow overwrite if user has delete permission # (avoids win2000 freaking out and deleting the server copy without uploading its own) try: self.rv2a(filename, False, True, False, True) do_it = True except: do_it = False if not do_it: raise FSE("File already exists") # Don't unlink file for append mode elif "a" not in mode: wunlink(self.log, ap, VF_CAREFUL) ret = open(fsenc(ap), mode, self.args.iobuf) if w and "fperms" in vfs.flags: set_fperms(ret, vfs.flags) return ret def chdir(self, path: str) -> None: nwd = join(self.cwd, path) vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False) if not vfs.realpath: self.cwd = nwd return ap = vfs.canonical(rem) try: st = bos.stat(ap) if not stat.S_ISDIR(st.st_mode): raise Exception() except: # returning 550 is library-default and suitable raise FSE("No such file or directory") avfs = vfs.chk_ap(ap, st) if not avfs: raise FSE("Permission denied", 1) self.cwd = nwd def mkdir(self, path: str) -> None: ap, vfs, _ = self.rv2a(path, w=True) bos.makedirs(ap, vf=vfs.flags) # filezilla expects this def listdir(self, path: str) -> list[str]: vpath = join(self.cwd, path) try: ap, vfs, rem = self.v2a(vpath, True, False) if not bos.path.isdir(ap): raise FSE("No such file or directory", 1) fsroot, vfs_ls1, vfs_virt = vfs.ls( rem, self.uname, not self.args.no_scandir, [[True, False], [False, True]], throw=True, ) vfs_ls = [x[0] for x in vfs_ls1] vfs_ls.extend(vfs_virt.keys()) if self.uname not in vfs.axs.udot: vfs_ls = exclude_dotfiles(vfs_ls) vfs_ls.sort() return vfs_ls except Exception as ex: # panic on malicious names if getattr(ex, "severity", 0): raise if vpath.strip("/"): # display write-only folders as empty return [] # return list of accessible volumes ret = [] for vn in self.hub.asrv.vfs.all_vols.values(): if "/" in vn.vpath or not vn.vpath: continue # only include toplevel-mounted vols try: self.hub.asrv.vfs.get(vn.vpath, self.uname, True, False) ret.append(vn.vpath) except: pass ret.sort() return ret def rmdir(self, path: str) -> None: ap = self.rv2a(path, d=True)[0] try: bos.rmdir(ap) except OSError as e: if e.errno != errno.ENOENT: raise def remove(self, path: str) -> None: if self.args.no_del: raise FSE("The delete feature is disabled in server config") vp = join(self.cwd, path).lstrip("/") try: self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False) except Exception as ex: raise FSE(str(ex)) def rename(self, src: str, dst: str) -> None: if self.args.no_mv: raise FSE("The rename/move feature is disabled in server config") svp = join(self.cwd, src).lstrip("/") dvp = join(self.cwd, dst).lstrip("/") try: self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp) except Exception as ex: raise FSE(str(ex)) def chmod(self, path: str, mode: str) -> None: pass def stat(self, path: str) -> os.stat_result: try: ap = self.rv2a(path, r=True)[0] return bos.stat(ap) except FSE as ex: if ex.severity: raise ap = self.rv2a(path)[0] st = bos.stat(ap) if not stat.S_ISDIR(st.st_mode): raise return st def utime(self, path: str, timeval: float) -> None: ap = self.rv2a(path, w=True)[0] bos.utime_c(logging.warning, ap, int(timeval), False) def lstat(self, path: str) -> os.stat_result: ap = self.rv2a(path)[0] return bos.stat(ap) def isfile(self, path: str) -> bool: try: st = self.stat(path) return stat.S_ISREG(st.st_mode) except Exception as ex: if getattr(ex, "severity", 0): raise return False # expected for mojibake in ftp_SIZE() def islink(self, path: str) -> bool: ap = self.rv2a(path)[0] return bos.path.islink(ap) def isdir(self, path: str) -> bool: try: st = self.stat(path) return stat.S_ISDIR(st.st_mode) except Exception as ex: if getattr(ex, "severity", 0): raise return True def getsize(self, path: str) -> int: ap = self.rv2a(path)[0] return bos.path.getsize(ap) def getmtime(self, path: str) -> float: ap = self.rv2a(path)[0] return bos.path.getmtime(ap) def realpath(self, path: str) -> str: return path def lexists(self, path: str) -> bool: ap = self.rv2a(path)[0] return bos.path.lexists(ap) def get_user_by_uid(self, uid: int) -> str: return "root" def get_group_by_uid(self, gid: int) -> str: return "root" class FtpHandler(FTPHandler): abstracted_fs = FtpFs hub: "SvcHub" args: argparse.Namespace uname: str def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None: self.hub: "SvcHub" = FtpHandler.hub self.args: argparse.Namespace = FtpHandler.args self.uname = "*" if PY2: FTPHandler.__init__(self, conn, server, ioloop) else: super(FtpHandler, self).__init__(conn, server, ioloop) cip = self.remote_ip if cip.startswith("::ffff:"): cip = cip[7:] if self.args.ftp_ipa_nm and not self.args.ftp_ipa_nm.map(cip): logging.warning("client rejected (--ftp-ipa): %s", cip) self.connected = False conn.close() return self.cli_ip = cip # abspath->vpath mapping to resolve log_transfer paths self.vfs_map: dict[str, str] = {} # reduce non-debug logging self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")] def ftp_STOR(self, file: str, mode: str = "w") -> Any: # Optional[str] vp = join(self.fs.cwd, file).lstrip("/") try: ap, vfs, rem = self.fs.v2a(vp, w=True) except Exception as ex: self.respond("550 %s" % (ex,), logging.info) return self.vfs_map[ap] = vp xbu = vfs.flags.get("xbu") if xbu: hr = runhook( None, None, self.hub.up2k, "xbu.ftpd", xbu, ap, vp, "", self.uname, self.hub.asrv.vfs.get_perms(vp, self.uname), 0, 0, self.cli_ip, time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "Upload blocked by xbu server config: %r" % (vp,) self.respond("550 %s" % (t,), logging.info) return # print("ftp_STOR: {} {} => {}".format(vp, mode, ap)) ret = FTPHandler.ftp_STOR(self, file, mode) # print("ftp_STOR: {} {} OK".format(vp, mode)) return ret def log_transfer( self, cmd: str, filename: bytes, receive: bool, completed: bool, elapsed: float, bytes: int, ) -> Any: # None ap = filename.decode("utf-8", "replace") vp = self.vfs_map.pop(ap, None) # print("xfer_end: {} => {}".format(ap, vp)) if vp: vp, fn = os.path.split(vp) vfs, rem = self.hub.asrv.vfs.get(vp, self.uname, False, True) vfs, rem = vfs.get_dbv(rem) self.hub.up2k.hash_file( vfs.realpath, vfs.vpath, vfs.flags, rem, fn, self.cli_ip, time.time(), self.uname, ) return FTPHandler.log_transfer( self, cmd, filename, receive, completed, elapsed, bytes ) try: from pyftpdlib.handlers import TLS_FTPHandler class SftpHandler(FtpHandler, TLS_FTPHandler): pass except: pass class Ftpd(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.args = hub.args hs = [] if self.args.ftp: hs.append([FtpHandler, self.args.ftp]) if self.args.ftps: try: h1 = SftpHandler except: t = "\nftps requires pyopenssl;\nplease run the following:\n\n {} -m pip install --user pyopenssl\n" print(t.format(pybin)) sys.exit(1) h1.certfile = self.args.cert h1.tls_control_required = True h1.tls_data_required = True hs.append([h1, self.args.ftps]) for h_lp in hs: h2, lp = h_lp FtpHandler.hub = h2.hub = hub FtpHandler.args = h2.args = hub.args FtpHandler.authorizer = h2.authorizer = FtpAuth(hub) if self.args.ftp_pr: p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")] if self.args.ftp and self.args.ftps: # divide port range in half d = int((p2 - p1) / 2) if lp == self.args.ftp: p2 = p1 + d else: p1 += d + 1 h2.passive_ports = list(range(p1, p2 + 1)) if self.args.ftp_nat: h2.masquerade_address = self.args.ftp_nat lgr = logging.getLogger("pyftpdlib") lgr.setLevel(logging.DEBUG if self.args.ftpv else logging.INFO) ips = self.args.ftp_i if "::" in ips: ips.append("0.0.0.0") ips = [x for x in ips if not x.startswith(("unix:", "fd:"))] if self.args.ftp4: ips = [x for x in ips if ":" not in x] if not ips: lgr.fatal("cannot start ftp-server; no compatible IPs in -i") return ips = list(ODict.fromkeys(ips)) # dedup ioloop = IOLoop() for ip in ips: for h, lp in hs: try: FTPServer((ip, int(lp)), h, ioloop) except: if ip != "0.0.0.0" or "::" not in ips: raise Daemon(ioloop.loop, "ftp") def join(p1: str, p2: str) -> str: w = os.path.join(p1, p2.replace("\\", "/")) return os.path.normpath(w).replace("\\", "/") ================================================ FILE: copyparty/httpcli.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse # typechk import copy import errno import hashlib import itertools import json import os import random import re import socket import stat import sys import threading # typechk import time import uuid from datetime import datetime from operator import itemgetter import jinja2 # typechk from ipaddress import IPv6Network try: if os.environ.get("PRTY_NO_LZMA"): raise Exception() import lzma except: pass from .__init__ import ANYWIN, RES, RESM, TYPE_CHECKING, EnvParams, unicode from .__version__ import S_VERSION from .authsrv import LEELOO_DALLAS, VFS # typechk from .bos import bos from .qrkode import QrCode, qr2svg, qrgen from .star import StreamTar from .sutil import StreamArc, gfilter from .szip import StreamZip from .up2k import up2k_chunksize from .util import unquote # type: ignore from .util import ( APPLESAN_RE, BITNESS, DAV_ALLPROPS, E_SCK_WR, HAVE_SQLITE3, HTTPCODE, SAFE_MIMES, UTC, VPTL_MAC, VPTL_OS, VPTL_WIN, Garda, MultipartParser, ODict, Pebkac, UnrecvEOF, WrongPostKey, absreal, afsenc, alltrace, atomic_move, b64dec, eol_conv, exclude_dotfiles, exclude_dotfiles_ls, formatdate, fsenc, gen_content_disposition, gen_filekey, gen_filekey_dbg, gencookie, get_df, get_spd, guess_mime, gzip, gzip_file_orig_sz, gzip_orig_sz, has_resource, hashcopy, hidedir, html_bescape, html_escape, html_sh_esc, humansize, ipnorm, json_hesc, justcopy, load_resource, loadpy, log_reloc, min_ex, open_nolock, pathmod, quotep, rand_name, read_header, read_socket, read_socket_chunked, read_socket_unbounded, read_utf8, relchk, ren_open, runhook, s2hms, s3enc, safe_mime, sanitize_fn, sanitize_vpath, sendfile_kern, sendfile_py, set_fperms, stat_resource, str_anchor, ub64dec, ub64enc, ujoin, undot, unescape_cookie, unquotep, vjoin, vol_san, vroots, vsplit, wunlink, yieldfile, ) if True: # pylint: disable=using-constant-test import typing from typing import ( Any, Generator, Iterable, Match, Optional, Pattern, Sequence, Type, Union, ) if TYPE_CHECKING: from .httpconn import HttpConn if not hasattr(socket, "AF_UNIX"): setattr(socket, "AF_UNIX", -9001) _ = (argparse, threading) USED4SEC = {"usedforsecurity": False} if sys.version_info > (3, 9) else {} ALL_COOKIES = "k304 no304 js idxh dots cppwd cppws".split() BADXFF = " due to dangerous misconfiguration (the http-header specified by --xff-hdr was received from an untrusted reverse-proxy)" BADXFF2 = ". Some copyparty features are now disabled as a safety measure.\n\n\n" BADXFP = ', or change the copyparty global-option "xf-proto" to another header-name to read this value from. Alternatively, if your reverseproxy is not able to provide a header similar to "X-Forwarded-Proto", then you must tell copyparty which protocol to assume; either "--xf-proto-fb=http" or "--xf-proto-fb=https"' BADXFFB = "NOTE: serverlog has a message regarding your reverse-proxy config" BADVER = 'Please upgrade copyparty; Your version has a vulnerability

(only users with permission "a" or "A" can see this message)

' H_CONN_KEEPALIVE = "Connection: Keep-Alive" H_CONN_CLOSE = "Connection: Close" RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"} ACODE2_FMT = set(["opus", "owa", "caf", "mp3", "flac", "wav"]) IDX_HTML = set(["index.htm", "index.html"]) A_FILE = os.stat_result( (0o644, -1, -1, 1, 1000, 1000, 8, 0x39230101, 0x39230101, 0x39230101) ) RE_CC = re.compile(r"[\x00-\x1f\x7f]") # search always faster RE_USAFE = re.compile(r'[\x00-\x1f\x7f<>"]') # search always faster RE_HSAFE = re.compile(r"[\x00-\x1f\x7f<>\"'&]") # search always much faster RE_HOST = re.compile(r"[^][0-9a-zA-Z.:_-]") # search faster <=17ch RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch RE_K = re.compile(r"[^0-9a-zA-Z_-]") # search faster <=17ch RE_HTTP1 = re.compile(r"(GET|HEAD|POST|PUT) [^ ]+ HTTP/1.1$") RE_HR = re.compile(r"[<>\"'&]") RE_MDV = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[Mm][Dd])$") RE_RSS_KW = re.compile(r"(\{[^} ]+\})") RE_SETCK = re.compile(r"[^0-9a-z=]") UPARAM_CC_OK = set("doc move tree".split()) PERMS_rwh = [ [True, False], [False, True], [False, False, False, False, False, False, True], ] def _build_zip_xcode() -> Sequence[str]: ret = "opus mp3 flac wav p".split() for codec in ("j", "w", "x"): for suf in ("", "f", "f3", "3"): ret.append("%s%s" % (codec, suf)) return ret ZIP_XCODE_L = _build_zip_xcode() ZIP_XCODE_S = set(ZIP_XCODE_L) def _arg2cfg(txt: str) -> str: return re.sub(r' "--([^=]{3,12})=', r' global-option "\1: ', txt) class HttpCli(object): """ Spawned by HttpConn to process one http transaction """ def __init__(self, conn: "HttpConn") -> None: assert conn.sr # !rm empty_stringlist: list[str] = [] self.t0 = time.time() self.conn = conn self.u2mutex = conn.u2mutex # mypy404 self.s = conn.s self.sr = conn.sr self.ip = conn.addr[0] self.addr: tuple[str, int] = conn.addr self.args = conn.args # mypy404 self.E: EnvParams = self.args.E self.asrv = conn.asrv # mypy404 self.ico = conn.ico # mypy404 self.thumbcli = conn.thumbcli # mypy404 self.u2fh = conn.u2fh # mypy404 self.pipes = conn.pipes # mypy404 self.log_func = conn.log_func # mypy404 self.log_src = conn.log_src # mypy404 self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey self.tls = self.is_https = hasattr(self.s, "cipher") self.is_vproxied = bool(self.args.R) # placeholders; assigned by run() self.keepalive = False self.in_hdr_recv = True self.headers: dict[str, str] = {} self.mode = " " # http verb self.req = " " self.http_ver = "" self.hint = "" self.host = " " self.ua = " " self.is_rclone = False self.ouparam: dict[str, str] = {} self.uparam: dict[str, str] = {} self.cookies: dict[str, str] = {} self.avn: Optional[VFS] = None self.vn = self.asrv.vfs self.rem = " " self.vpath = " " self.vpaths = " " self.dl_id = "" self.gctx = " " # additional context for garda self.trailing_slash = True self.uname = "*" self.pw = "" self.rvol = self.wvol = self.avol = empty_stringlist self.do_log = True self.can_read = False self.can_write = False self.can_move = False self.can_delete = False self.can_get = False self.can_upget = False self.can_html = False self.can_admin = False self.can_dot = False self.out_headerlist: list[tuple[str, str]] = [] self.out_headers: dict[str, str] = {} # post self.parser: Optional[MultipartParser] = None # end placeholders self.html_head = "" def log(self, msg: str, c: Union[int, str] = 0) -> None: ptn = self.asrv.re_pwd if ptn and ptn.search(msg): if self.asrv.ah.on: msg = ptn.sub("\033[7m pw \033[27m", msg) else: msg = ptn.sub(self.unpwd, msg) self.log_func(self.log_src, msg, c) def unpwd(self, m: Match[str]) -> str: a, b, c = m.groups() uname = self.asrv.iacct.get(b) or self.asrv.sesa.get(b) return "%s\033[7m %s \033[27m%s" % (a, uname, c) def _assert_safe_rem(self, rem: str) -> None: # sanity check to prevent any disasters # (this function hopefully serves no purpose; validation has already happened at this point, this only exists as a last-ditch effort just in case) if rem.startswith(("/", "../")) or "/../" in rem: raise Exception("that was close") def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str: return gen_filekey_dbg( alg, salt, fspath, fsize, inode, self.log, self.args.log_fk ) def j2s(self, name: str, **ka: Any) -> str: tpl = self.conn.hsrv.j2[name] ka["r"] = self.args.SR if self.is_vproxied else "" ka["ts"] = self.conn.hsrv.cachebuster() ka["lang"] = self.cookies.get("cplng") or self.args.lang ka["favico"] = self.args.favico ka["s_doctitle"] = self.args.doctitle ka["tcolor"] = self.vn.flags["tcolor"] if self.args.js_other and "js" not in ka: zs = self.args.js_other zs += "&" if "?" in zs else "?" ka["js"] = zs if "html_head_d" in self.vn.flags: ka["this"] = self self._build_html_head(ka) ka["html_head"] = self.html_head return tpl.render(**ka) # type: ignore def j2j(self, name: str) -> jinja2.Template: return self.conn.hsrv.j2[name] def run(self) -> bool: """returns true if connection can be reused""" self.out_headers = { "Cache-Control": "no-store, max-age=0", } if self.args.early_ban and self.is_banned(): return False if self.conn.ipa_nm and not self.conn.ipa_nm.map(self.conn.addr[0]): self.log("client rejected (--ipa)", 3) self.terse_reply(b"", 500) return False try: self.s.settimeout(2) headerlines = read_header(self.sr, self.args.s_thead, self.args.s_thead) self.in_hdr_recv = False if not headerlines: return False try: self.mode, self.req, self.http_ver = headerlines[0].split(" ") # normalize incoming headers to lowercase; # outgoing headers however are Correct-Case for header_line in headerlines[1:]: k, zs = header_line.split(":", 1) self.headers[k.lower()] = zs.strip() if zs.endswith(" HTTP/1.1") and RE_HTTP1.search(zs): raise Exception() except: headerlines = [repr(x) for x in headerlines] msg = "#[ " + " ]\n#[ ".join(headerlines) + " ]" raise Pebkac(400, "bad headers", log=msg) except Pebkac as ex: self.mode = "GET" self.req = "[junk]" self.http_ver = "HTTP/1.1" # self.log("pebkac at httpcli.run #1: " + repr(ex)) self.keepalive = False h = {"WWW-Authenticate": 'Basic realm="a"'} if ex.code == 401 else {} try: self.loud_reply(unicode(ex), status=ex.code, headers=h, volsan=True) except: pass if ex.log: self.log("additional error context:\n" + ex.log, 6) return False self.sr.nb = 0 self.conn.hsrv.nreq += 1 self.ua = self.headers.get("user-agent", "") self.is_rclone = self.ua.startswith("rclone/") zs = self.headers.get("connection", "").lower() self.keepalive = "close" not in zs and ( self.http_ver != "HTTP/1.0" or zs == "keep-alive" ) if ( "transfer-encoding" in self.headers and self.headers["transfer-encoding"].lower() != "identity" ): self.sr.te = 1 if "content-length" in self.headers: # rfc9112:6.2: ignore CL if TE self.keepalive = False self.headers.pop("content-length") t = "suspicious request (has both TE and CL); ignoring CL and disabling keepalive" self.log(t, 3) self.host = self.headers.get("host") or "" if not self.host: if self.s.family == socket.AF_UNIX: self.host = self.args.name else: zs = "%s:%s" % self.s.getsockname()[:2] self.host = zs[7:] if zs.startswith("::ffff:") else zs trusted_xff = False n = self.args.rproxy if n: zso = self.headers.get(self.args.xff_hdr) if zso: if n > 0: n -= 1 zsl = zso.split(",") try: cli_ip = zsl[n].strip() except: cli_ip = self.ip self.bad_xff = True if self.args.rproxy != 9999999: t = "global-option --rproxy %d could not be used (out-of-bounds) for the received header [%s]" self.log(t % (self.args.rproxy, zso) + BADXFF2, c=3) else: zsl = [ " rproxy: %d if this client's IP-address is [%s]" % (-1 - zd, zs.strip()) for zd, zs in enumerate(zsl[::-1]) ] t = 'could not determine the client\'s IP-address because the global-option --rproxy has not been configured, so the request-header [%s] specified by global-option --xff-hdr cannot be used safely! The raw header value was [%s]. Please see the "reverse-proxy" section in the readme. The best approach is to configure your reverse-proxy to give copyparty the exact IP-address to assume (perhaps in another header), but you may also try the following:' t = t % (self.args.xff_hdr, zso) t = "%s\n\n%s\n" % (t, "\n".join(zsl)) zs = self.headers.get(self.args.xf_proto) t2 = "\nFurthermore, the following request-headers are also relevant, and you should check that the values below are sensible:\n\n request-header [%s] (configured with global-option --xf-proto) has the value [%s]; this should be the protocol that the webbrowser is using, so either 'http' or 'https'" t += t2 % (self.args.xf_proto, zs or "NOT-PROVIDED") if not zs: t += ". Because the header is not provided by the reverse-proxy, you must either fix the reverseproxy config" t += BADXFP zs = self.headers.get(self.args.xf_host) t2 = "\n\n request-header [%s] (configured with global-option --xf-host) has the value [%s]; this should be the website domain or external IP-address which the webbrowser is accessing" t += t2 % (self.args.xf_host, zs or "NOT-PROVIDED") if not zs: zs = self.headers.get("host") t2 = ". Because the header is not provided by the reverse-proxy, copyparty is using the standard [Host] header which has the value [%s]" t += t2 % (zs or "NOT-PROVIDED") if zs: t += ". If that is the address that visitors are supposed to use to access your server -- or, in other words, it is not some internal address you wish to keep secret -- then the current choice of using the [Host] header is fine (usually the case)" if self.args.c: t = _arg2cfg(t) self.log(t + "\n\n\n", 3) pip = self.conn.addr[0] xffs = self.conn.xff_nm if xffs and not xffs.map(pip): t = 'got header "%s" from untrusted source "%s" claiming the true client ip is "%s" (raw value: "%s"); if you trust this, you must allowlist this proxy with "--xff-src=%s"%s' if self.headers.get("cf-connecting-ip"): t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"' else: t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"' t += BADXFF2 if "." in pip: zs = ".".join(pip.split(".")[:2]) + ".0.0/16" else: zs = IPv6Network(pip + "/64", False).compressed zs2 = ' or "--xff-src=lan"' if self.conn.xff_lan.map(pip) else "" t = t % (self.args.xff_hdr, pip, cli_ip, zso, zs, zs2) if self.args.c: t = _arg2cfg(t) self.log(t, 3) self.bad_xff = True else: self.ip = cli_ip self.log_src = self.conn.set_rproxy(self.ip) self.host = self.headers.get(self.args.xf_host, self.host) try: self.is_https = len(self.headers[self.args.xf_proto]) == 5 except: if self.args.xf_proto_fb: self.is_https = len(self.args.xf_proto_fb) == 5 else: self.bad_xff = True self.host = "example.com" t = 'got proxied request without header "%s" (global-option "xf-proto"). This header must contain either "http" or "https". Either fix your reverse-proxy config to include this header%s%s' t = t % (self.args.xf_proto, BADXFP, BADXFF2) if self.args.c: t = _arg2cfg(t) self.log(t, 3) # the semantics of trusted_xff and bad_xff are different; # trusted_xff is whether the connection came from a trusted reverseproxy, # regardless of whether the client ip detection is correctly configured # (the primary safeguard for idp is --idp-h-key) trusted_xff = True m = RE_HOST.search(self.host) if m and self.host != self.args.name: zs = self.host t = "malicious user; illegal Host header; req(%r) host(%r) => %r" self.log(t % (self.req, zs, zs[m.span()[0] :]), 1) self.cbonk(self.conn.hsrv.gmal, zs, "bad_host", "illegal Host header") self.terse_reply(b"illegal Host header", 400) return False if self.is_banned(): return False if self.conn.ipar_nm and not self.conn.ipar_nm.map(self.ip): self.log("client rejected (--ipar)", 3) self.terse_reply(b"", 500) return False if self.conn.aclose: nka = self.conn.aclose ip = ipnorm(self.ip) if ip in nka: rt = nka[ip] - time.time() if rt < 0: self.log("client uncapped", 3) del nka[ip] else: self.keepalive = False ptn: Optional[Pattern[str]] = self.conn.lf_url # mypy404 self.do_log = not ptn or not ptn.search(self.req) if self.args.ihead and self.do_log: keys = self.args.ihead if "*" in keys: keys = list(sorted(self.headers.keys())) for k in keys: zso = self.headers.get(k) if zso is not None: self.log("[H] {}: \033[33m[{}]".format(k, zso), 6) if "&" in self.req and "?" not in self.req: self.hint = "did you mean '?' instead of '&'" if self.args.uqe and "/.uqe/" in self.req: try: vpath, query = self.req.split("?")[0].split("/.uqe/") query = query.split("/")[0] # discard trailing junk # (usually a "filename" to trick discord into behaving) query = ub64dec(query.encode("utf-8")).decode("utf-8", "replace") if query.startswith("/"): self.req = "%s/?%s" % (vpath, query[1:]) else: self.req = "%s?%s" % (vpath, query) except Exception as ex: t = "bad uqe in request [%s]: %r" % (self.req, ex) self.loud_reply(t, status=400) return False m = RE_USAFE.search(self.req) if m: zs = self.req t = "malicious user; Cc in req0 %r => %r" self.log(t % (zs, zs[m.span()[0] :]), 1) self.cbonk(self.conn.hsrv.gmal, zs, "cc_r0", "Cc in req0") self.terse_reply(b"", 500) return False # split req into vpath + uparam uparam = {} if "?" not in self.req: vpath = unquotep(self.req) # not query, so + means + self.trailing_slash = vpath.endswith("/") vpath = undot(vpath) else: vpath, arglist = self.req.split("?", 1) vpath = unquotep(vpath) self.trailing_slash = vpath.endswith("/") vpath = undot(vpath) re_k = RE_K ptn_cc = RE_CC k_safe = UPARAM_CC_OK for k in arglist.split("&"): sv = "" if "=" in k: k, zs = k.split("=", 1) # x-www-form-urlencoded (url query part) uses # either + or %20 for 0x20 so handle both sv = unquotep(zs.strip().replace("+", " ")) m = re_k.search(k) if m: t = "malicious user; bad char in query key; req(%r) qk(%r) => %r" self.log(t % (self.req, k, k[m.span()[0] :]), 1) self.cbonk(self.conn.hsrv.gmal, self.req, "bc_q", "illegal qkey") self.terse_reply(b"", 500) return False k = k.lower() uparam[k] = sv if k in k_safe: continue zs = "%s=%s" % (k, sv) m = ptn_cc.search(zs) if not m: continue t = "malicious user; Cc in query; req(%r) qp(%r) => %r" self.log(t % (self.req, zs, zs[m.span()[0] :]), 1) self.cbonk(self.conn.hsrv.gmal, self.req, "cc_q", "Cc in query") self.terse_reply(b"", 500) return False if "k" in uparam: m = re_k.search(uparam["k"]) if m: zs = uparam["k"] t = "malicious user; illegal filekey; req(%r) k(%r) => %r" self.log(t % (self.req, zs, zs[m.span()[0] :]), 1) self.cbonk(self.conn.hsrv.gmal, zs, "bad_k", "illegal filekey") self.terse_reply(b"illegal filekey", 400) return False if self.is_vproxied: if vpath.startswith(self.args.R): vpath = vpath[len(self.args.R) + 1 :] else: t = "incorrect --rp-loc or webserver config; expected vpath starting with %r but got %r" self.log(t % (self.args.R, vpath), 1) self.is_vproxied = False self.ouparam = uparam.copy() if self.args.rsp_slp: time.sleep(self.args.rsp_slp) if self.args.rsp_jtr: time.sleep(random.random() * self.args.rsp_jtr) zso = self.headers.get("cookie") if zso: if len(zso) > self.args.cookie_cmax: self.loud_reply("cookie header too big", status=400) return False zsll = [x.lstrip().split("=", 1) for x in zso.split(";") if "=" in x] cookies = {k.rstrip(): unescape_cookie(zs.strip(), k) for k, zs in zsll} cookie_pw = cookies.get("cppws" if self.is_https else "cppwd") or "" if "b" in cookies and "b" not in uparam: uparam["b"] = cookies["b"] if len(cookies) > self.args.cookie_nmax: self.loud_reply("too many cookies", status=400) else: cookies = {} cookie_pw = "" if len(uparam) > 12: t = "http-request rejected; num.params: %d %r" self.log(t % (len(uparam), self.req), 3) self.loud_reply("u wot m8", status=400) return False if VPTL_OS: vpath = vpath.translate(VPTL_OS) self.uparam = uparam self.cookies = cookies self.vpath = vpath self.vpaths = vpath + "/" if self.trailing_slash and vpath else vpath if "qr" in uparam: return self.tx_qr() if "\x00" in vpath or (ANYWIN and ("\n" in vpath or "\r" in vpath)): self.log("illegal relpath; req(%r) => %r" % (self.req, "/" + self.vpath)) self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths") return self.tx_404() and False zso = self.headers.get("authorization") bauth = "" if ( zso and not self.args.no_bauth and (not cookie_pw or not self.args.bauth_last) ): try: zb = zso.split(" ")[1].encode("ascii") zs = b64dec(zb).decode("utf-8") # try "pwd", "x:pwd", "pwd:x" for bauth in [zs] + zs.split(":", 1)[::-1]: if bauth in self.asrv.sesa: break hpw = self.asrv.ah.hash(bauth) if self.asrv.iacct.get(hpw): break except: pass self.pw = ( uparam.get(self.args.pw_urlp) or self.headers.get(self.args.pw_hdr) or bauth or cookie_pw ) self.uname = ( self.asrv.sesa.get(self.pw) or self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*" ) if self.args.have_idp_hdrs and ( self.uname == "*" or self.args.ao_idp_before_pw ): idp_usr = "" if self.args.idp_hm_usr: for hn, hmv in self.args.idp_hm_usr_p.items(): zs = self.headers.get(hn) if zs: for zs1, zs2 in hmv.items(): if zs == zs1: idp_usr = zs2 break if idp_usr: break for hn in self.args.idp_h_usr: if idp_usr and not self.args.ao_h_before_hm: break idp_usr = self.headers.get(hn) or idp_usr if idp_usr: idp_grp = ( self.headers.get(self.args.idp_h_grp) or "" if self.args.idp_h_grp else "" ) if self.args.idp_chsub: idp_usr = idp_usr.translate(self.args.idp_chsub_tr) idp_grp = idp_grp.translate(self.args.idp_chsub_tr) if not trusted_xff: pip = self.conn.addr[0] xffs = self.conn.xff_nm trusted_xff = xffs and xffs.map(pip) trusted_key = ( not self.args.idp_h_key ) 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: t = 'the idp-h-key header ("%s") is not present in the request; will NOT trust the other headers saying that the client\'s username is "%s" and group is "%s"' self.log(t % (self.args.idp_h_key, idp_usr, idp_grp), 3) if not trusted_xff: t = 'got IdP headers from untrusted source "%s" claiming the client\'s username is "%s" and group is "%s"; if you trust this, you must allowlist this proxy with "--xff-src=%s"%s' if not self.args.idp_h_key: t += " Note: you probably also want to specify --idp-h-key for additional security" pip = self.conn.addr[0] zs = ( ".".join(pip.split(".")[:2]) + "." if "." in pip else ":".join(pip.split(":")[:4]) + ":" ) + "0.0/16" zs2 = ( ' or "--xff-src=lan"' if self.conn.xff_lan.map(pip) else "" ) self.log(t % (pip, idp_usr, idp_grp, zs, zs2), 3) idp_usr = "*" idp_grp = "" if idp_usr in self.asrv.vfs.aread: self.pw = "" self.uname = idp_usr if self.args.ao_have_pw or self.args.idp_logout: self.html_head += "\n" else: self.html_head += "\n" zs = self.asrv.ases.get(idp_usr) if zs: self.set_idp_cookie(zs) else: self.log("unknown username: %r" % (idp_usr,), 1) if self.args.have_ipu_or_ipr: if self.args.ipu and (self.uname == "*" or self.args.ao_ipu_wins): self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)] ipr = self.conn.hsrv.ipr if ipr and self.uname in ipr: if not ipr[self.uname].map(self.ip): self.log("username [%s] rejected by --ipr" % (self.uname,), 3) self.uname = "*" self.rvol = self.asrv.vfs.aread[self.uname] self.wvol = self.asrv.vfs.awrite[self.uname] self.avol = self.asrv.vfs.aadmin[self.uname] if self.pw and ( self.pw != cookie_pw or self.conn.freshen_pwd + 30 < time.time() ): self.conn.freshen_pwd = time.time() self.get_pwd_cookie(self.pw) if self.is_rclone: # dots: always include dotfiles if permitted # lt: probably more important showing the correct timestamps of any dupes it just uploaded rather than the lastmod time of any non-copyparty-managed symlinks # b: basic-browser if it tries to parse the html listing uparam["dots"] = "" uparam["lt"] = "" uparam["b"] = "" cookies["b"] = "" vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags): ap = vn.canonical(rem) avn = vn.chk_ap(ap) else: avn = vn if "bcasechk" in vn.flags and not vn.casechk(rem, True): return self.tx_404() and False try: assert avn # type: ignore # !rm ( self.can_read, self.can_write, self.can_move, self.can_delete, self.can_get, self.can_upget, self.can_html, self.can_admin, self.can_dot, ) = avn.uaxs[self.uname] except: pass # default is all-false self.avn = avn self.vn = vn # note: do not dbv due to walk/zipgen self.rem = rem self.s.settimeout(self.args.s_tbody or None) if "html_head_s" in vn.flags: self.html_head += vn.flags["html_head_s"] try: cors_k = self._cors() if self.mode in ("GET", "HEAD"): return self.handle_get() and self.keepalive if self.mode == "OPTIONS": return self.handle_options() and self.keepalive if not cors_k: host = self.headers.get("host", "") origin = self.headers.get("origin", "") proto = "https://" if self.is_https else "http://" guess = "modifying" if (origin and host) else "stripping" t = "cors-reject %s because request-header Origin=%r does not match request-protocol %r and host %r based on request-header Host=%r (note: if this request is not malicious, check if your reverse-proxy is accidentally %s request headers, in particular 'Origin', for example by running copyparty with --ihead='*' to show all request headers)" self.log(t % (self.mode, origin, proto, self.host, host, guess), 3) raise Pebkac(403, "rejected by cors-check (see fileserver log)") # getattr(self.mode) is not yet faster than this if self.mode == "POST": return self.handle_post() and self.keepalive elif self.mode == "PUT": return self.handle_put() and self.keepalive elif self.mode == "PROPFIND": return self.handle_propfind() and self.keepalive elif self.mode == "DELETE": return self.handle_delete() and self.keepalive elif self.mode == "PROPPATCH": return self.handle_proppatch() and self.keepalive elif self.mode == "LOCK": return self.handle_lock() and self.keepalive elif self.mode == "UNLOCK": return self.handle_unlock() and self.keepalive elif self.mode == "MKCOL": return self.handle_mkcol() and self.keepalive elif self.mode in ("MOVE", "COPY"): return self.handle_cpmv() and self.keepalive else: raise Pebkac(400, "invalid HTTP verb %r" % (self.mode,)) except Exception as ex: if not isinstance(ex, Pebkac): pex = Pebkac(500) else: pex: Pebkac = ex # type: ignore try: if pex.code == 999: self.terse_reply(b"", 500) return False post = ( self.mode in ("POST", "PUT") or "content-length" in self.headers or self.sr.te ) if pex.code >= (300 if post else 400): self.keepalive = False em = str(ex) msg = em if pex is ex else min_ex() if pex.code != 404 or self.do_log: self.log( "http%d: %s\033[0m, %r" % (pex.code, msg, "/" + self.vpath), 6 if em.startswith("client d/c ") else 3, ) if self.hint and self.hint.startswith(" "): if self.args.log_badxml: t = "invalid XML received from client: %r" self.log(t % (self.hint[6:],), 6) else: t = "received invalid XML from client; enable --log-badxml to see the whole XML in the log" self.log(t, 6) self.hint = "" msg = "%s\r\nURL: %s\r\n" % (em, self.vpath) if self.hint: msg += "hint: %s\r\n" % (self.hint,) if "database is locked" in em: self.conn.hsrv.broker.say("log_stacks") msg += "hint: important info in the server log\r\n" zb = b"
" + html_escape(msg).encode("utf-8", "replace")
                h = {"WWW-Authenticate": 'Basic realm="a"'} if pex.code == 401 else {}
                self.reply(zb, status=pex.code, headers=h, volsan=True)
                if pex.log:
                    self.log("additional error context:\n" + pex.log, 6)

                return self.keepalive
            except Pebkac:
                return False

        finally:
            if self.dl_id:
                self.conn.hsrv.dli.pop(self.dl_id, None)
                self.conn.hsrv.dls.pop(self.dl_id, None)

    def dip(self) -> str:
        if self.args.plain_ip:
            return self.ip.replace(":", ".")
        else:
            return self.conn.iphash.s(self.ip)

    def cbonk(self, g: Garda, v: str, reason: str, descr: str) -> bool:
        cond = self.args.dont_ban
        if (
            cond == "any"
            or (cond == "auth" and self.uname != "*")
            or (cond == "aa" and self.avol)
            or (cond == "av" and self.can_admin)
            or (cond == "rw" and self.can_read and self.can_write)
        ):
            return False

        self.conn.hsrv.nsus += 1
        if not g.lim:
            return False

        bonk, ip = g.bonk(self.ip, v + self.gctx)
        if not bonk:
            return False

        xban = self.vn.flags.get("xban")
        if xban:
            hr = runhook(
                self.log,
                self.conn.hsrv.broker,
                None,
                "xban",
                xban,
                self.vn.canonical(self.rem),
                self.vpath,
                self.host,
                self.uname,
                "",
                time.time(),
                0,
                self.ip,
                time.time(),
                [reason, reason],
            )
            if hr.get("rv") == 0:
                return False

        self.log("client banned: %s" % (descr,), 1)
        self.conn.hsrv.bans[ip] = bonk
        self.conn.hsrv.nban += 1
        return True

    def is_banned(self) -> bool:
        if not self.conn.bans:
            return False

        bans = self.conn.bans
        ip = ipnorm(self.ip)
        if ip not in bans:
            return False

        rt = bans[ip] - time.time()
        if rt < 0:
            del bans[ip]
            self.log("client unbanned", 3)
            return False

        self.log("banned for {:.0f} sec".format(rt), 6)
        self.terse_reply(self.args.banmsg_b, 403)
        return True

    def permit_caching(self) -> None:
        cache = self.uparam.get("cache")
        if cache is None:
            self.out_headers["Cache-Control"] = self.vn.flags["cachectl"]
            return

        n = 69 if not cache else 604869 if cache == "i" else int(cache)
        self.out_headers["Cache-Control"] = "max-age=" + str(n)

    def k304(self) -> bool:
        k304 = self.cookies.get("k304")
        return k304 == "y" or (self.args.k304 == 2 and k304 != "n")

    def no304(self) -> bool:
        no304 = self.cookies.get("no304")
        return no304 == "y" or (self.args.no304 == 2 and no304 != "n")

    def _build_html_head(self, kv: dict[str, Any]) -> None:
        html = str(self.vn.flags["html_head_d"])
        is_jinja = html[:2] in "%@%"
        if is_jinja:
            html = html.replace("%", "", 1)

        if html.startswith("@"):
            html = read_utf8(self.log, html[1:], True)

        if html.startswith("%"):
            html = html[1:]
            is_jinja = True

        if is_jinja:
            with self.conn.hsrv.mutex:
                if html not in self.conn.hsrv.j2:
                    j2env = jinja2.Environment()
                    tpl = j2env.from_string(html)
                    self.conn.hsrv.j2[html] = tpl
                html = self.conn.hsrv.j2[html].render(**kv)

        self.html_head += html + "\n"

    def send_headers(
        self,
        oh_k: str,
        length: Optional[int],
        status: int = 200,
        mime: Optional[str] = None,
        headers: Optional[dict[str, str]] = None,
    ) -> None:
        response = ["%s %s %s" % (self.http_ver, status, HTTPCODE[status])]

        # headers{} overrides anything set previously
        if headers:
            self.out_headers.update(headers)

        if status == 304:
            self.out_headers.pop("Content-Length", None)
            self.out_headers.pop("Content-Type", None)
            self.out_headerlist[:] = []
            if self.k304():
                self.keepalive = False
        else:
            if length is not None:
                response.append("Content-Length: " + unicode(length))

            if mime:
                self.out_headers["Content-Type"] = mime
            elif "Content-Type" not in self.out_headers:
                self.out_headers["Content-Type"] = "text/html; charset=utf-8"

        # close if unknown length, otherwise take client's preference
        response.append(H_CONN_KEEPALIVE if self.keepalive else H_CONN_CLOSE)
        response.append("Date: " + formatdate())

        for k, zs in list(self.out_headers.items()) + self.out_headerlist:
            response.append("%s: %s" % (k, zs))

        ptn_cc = RE_CC
        for zs in response:
            m = ptn_cc.search(zs)
            if m:
                t = "malicious user; Cc in out-hdr; req(%r) hdr(%r) => %r"
                self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)
                self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
                raise Pebkac(999)

        response.append(self.vn.flags[oh_k])

        if self.args.ohead and self.do_log:
            zs = response.pop()[:-4]
            response.extend(zs.split("\r\n"))
            keys = self.args.ohead
            if "*" in keys:
                lines = response[1:]
            else:
                lines = []
                for zs in response[1:]:
                    if zs.split(":")[0].lower() in keys:
                        lines.append(zs)
            for zs in lines:
                hk, hv = zs.split(": ")
                self.log("[O] {}: \033[33m[{}]".format(hk, hv), 5)
            response.append("\r\n")

        try:
            self.s.sendall("\r\n".join(response).encode("utf-8"))
        except:
            raise Pebkac(400, "client d/c while replying headers")

    def reply(
        self,
        body: bytes,
        status: int = 200,
        mime: Optional[str] = None,
        headers: Optional[dict[str, str]] = None,
        volsan: bool = False,
    ) -> bytes:
        if (
            status > 400
            and status in (403, 404, 422)
            and (
                status != 422
                or (
                    not body.startswith(b"
partial upload exists")
                    and not body.startswith(b"
source file busy")
                )
            )
            and (status != 404 or (self.can_get and not self.can_read))
        ):
            if status == 404:
                g = self.conn.hsrv.g404
            elif status == 403:
                g = self.conn.hsrv.g403
            else:
                g = self.conn.hsrv.g422

            gurl = self.conn.hsrv.gurl
            if (
                gurl.lim
                and (not g.lim or gurl.lim < g.lim)
                and self.args.sus_urls.search(self.vpath)
            ):
                g = self.conn.hsrv.gurl

            if g.lim and (
                g == self.conn.hsrv.g422
                or not self.args.nonsus_urls
                or not self.args.nonsus_urls.search(self.vpath)
            ):
                self.cbonk(g, self.vpath, str(status), "%ss" % (status,))

        if volsan:
            vols = list(self.asrv.vfs.all_vols.values())
            body = vol_san(vols, body)
            try:
                zs = absreal(__file__).rsplit(os.path.sep, 2)[0]
                body = body.replace(zs.encode("utf-8"), b"PP")
            except:
                pass

        self.send_headers("oh_g", len(body), status, mime, headers)

        try:
            if self.mode != "HEAD":
                self.s.sendall(body)
        except:
            raise Pebkac(400, "client d/c while replying body")

        return body

    def loud_reply(self, body: str, *args: Any, **kwargs: Any) -> None:
        if not kwargs.get("mime"):
            kwargs["mime"] = "text/plain; charset=utf-8"

        self.log(body.rstrip())
        self.reply(body.encode("utf-8") + b"\r\n", *list(args), **kwargs)

    def terse_reply(self, body: bytes, status: int = 200) -> None:
        self.keepalive = False

        lines = [
            "%s %s %s" % (self.http_ver or "HTTP/1.1", status, HTTPCODE[status]),
            H_CONN_CLOSE,
        ]

        if body:
            lines.append(
                "Content-Type: text/html; charset=utf-8\r\nContent-Length: "
                + unicode(len(body))
            )

        lines.append("\r\n")
        self.s.sendall("\r\n".join(lines).encode("utf-8") + body)

    def urlq(self, add: dict[str, str], rm: list[str]) -> str:
        """
        generates url query based on uparam (b, pw, all others)
        removing anything in rm, adding pairs in add

        also list faster than set until ~20 items
        """

        if self.is_rclone:
            return ""

        kv = {k: zs for k, zs in self.uparam.items() if k not in rm}
        # no reason to consider args.pw_urlp
        if "pw" in kv:
            pw = self.cookies.get("cppws") or self.cookies.get("cppwd")
            if kv["pw"] == pw:
                del kv["pw"]

        kv.update(add)
        if not kv:
            return ""

        r = ["%s=%s" % (quotep(k), quotep(zs)) if zs else k for k, zs in kv.items()]
        return "?" + "&".join(r)

    def ourlq(self) -> str:
        # no reason to consider args.pw_urlp
        skip = ("pw", "h", "k")
        ret = []
        for k, v in self.ouparam.items():
            if k in skip:
                continue

            t = "%s=%s" % (quotep(k), quotep(v))
            ret.append(t.replace(" ", "+").rstrip("="))

        if not ret:
            return ""

        return "?" + "&".join(ret)

    def redirect(
        self,
        vpath: str,
        suf: str = "",
        msg: str = "aight",
        flavor: str = "go to",
        click: bool = True,
        status: int = 200,
        use302: bool = False,
    ) -> bool:
        vp = self.args.SRS + vpath
        html = self.j2s(
            "msg",
            h2='{} {}'.format(
                quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf
            ),
            pre=msg,
            click=click,
        ).encode("utf-8", "replace")

        if use302:
            self.reply(html, status=302, headers={"Location": vp})
        else:
            self.reply(html, status=status)

        return True

    def _cors(self) -> bool:
        ih = self.headers
        origin = ih.get("origin")
        if not origin:
            sfsite = ih.get("sec-fetch-site")
            if sfsite and sfsite.lower().startswith("cross"):
                origin = ":|"  # sandboxed iframe
            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()
        proto = "https" if self.is_https else "http"
        good_origins = self.args.acao + ["%s://%s" % (proto, host)]

        if (
            self.args.pw_hdr in ih
            or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins
        ):
            good_origin = True
            bad_hdrs = ("",)
        else:
            good_origin = False
            bad_hdrs = ("", self.args.pw_hdr)

        # '*' blocks auth through cookies / WWW-Authenticate;
        # exact-match for Origin is necessary to unlock those,
        # but the ?pw= param and PW: header are always allowed
        acah = ih.get("access-control-request-headers", "")
        acao = (origin if good_origin else None) or (
            "*" if "*" in good_origins else None
        )
        if self.args.allow_csrf:
            acao = origin or acao or "*"  # explicitly permit impersonation
            acam = ", ".join(self.conn.hsrv.mallow)  # and all methods + headers
            oh["Access-Control-Allow-Credentials"] = "true"
            good_origin = True
        else:
            acam = ", ".join(self.args.acam)
            # wash client-requested headers and roll with that
            if "range" not in acah.lower():
                acah += ",Range"  # firefox
            req_h = acah.split(",")
            req_h = [x.strip() for x in req_h]
            req_h = [x for x in req_h if x.lower() not in bad_hdrs]
            acah = ", ".join(req_h)

        if not acao:
            return False

        oh["Access-Control-Allow-Origin"] = acao
        oh["Access-Control-Allow-Methods"] = acam.upper()
        if acah:
            oh["Access-Control-Allow-Headers"] = acah

        return good_origin

    def handle_get(self) -> bool:
        if self.do_log:
            logmsg = "%-4s %s @%s" % (self.mode, self.req, self.uname)

            if "range" in self.headers:
                try:
                    rval = self.headers["range"].split("=", 1)[1]
                except:
                    rval = self.headers["range"]

                logmsg += " [\033[36m" + rval + "\033[0m]"

            self.log(logmsg)
            if "%" in self.req:
                self.log(" `-- %r" % (self.vpath,))

        # "embedded" resources
        if self.vpath.startswith(".cpr"):
            if self.vpath.startswith(".cpr/ico/"):
                return self.tx_ico(self.vpath.split("/")[-1], exact=True)

            if self.vpath.startswith(".cpr/ssdp"):
                if self.conn.hsrv.ssdp:
                    return self.conn.hsrv.ssdp.reply(self)
                else:
                    self.reply(b"ssdp is disabled in server config", 404)
                    return False

            if self.vpath == ".cpr/metrics":
                return self.conn.hsrv.metrics.tx(self)

            if self.vpath.startswith(".cpr/w/"):
                res_path = "web/" + self.vpath[7:]
            else:
                res_path = "web/" + self.vpath[5:]

            if res_path in RES:
                ap = self.E.mod_ + res_path
                if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
                    return self.tx_file("oh_g", ap)
                else:
                    return self.tx_res(res_path)

            if res_path in RESM:
                ap = self.E.mod_ + RESM[res_path]
                if (
                    "txt" not in self.uparam
                    and "mime" not in self.uparam
                    and not self.ouparam.get("dl")
                ):
                    # return mimetype matching request extension
                    self.ouparam["dl"] = res_path.split("/")[-1]
                if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
                    return self.tx_file("oh_g", ap)
                else:
                    return self.tx_res(res_path)

            self.tx_404()
            return False

        if "cf_challenge" in self.uparam:
            self.reply(self.j2s("cf").encode("utf-8", "replace"))
            return True

        if not self.can_read and not self.can_write and not self.can_get:
            t = "@%s has no access to %r"

            if self.vn.realpath and "on403" in self.vn.flags:
                t += " (on403)"
                self.log(t % (self.uname, "/" + self.vpath))
                ret = self.on40x(self.vn.flags["on403"], self.vn, self.rem)
                if ret == "true":
                    return True
                elif ret == "false":
                    return False
                elif ret == "home":
                    self.uparam["h"] = ""
                elif ret == "allow":
                    self.log("plugin override; access permitted")
                    self.can_read = self.can_write = self.can_move = True
                    self.can_delete = self.can_get = self.can_upget = True
                    self.can_admin = True
                else:
                    return self.tx_404(True)
            else:
                if (
                    self.asrv.badcfg1
                    and "h" not in self.ouparam
                    and "hc" not in self.ouparam
                ):
                    zs1 = "copyparty refused to start due to a failsafe: invalid server config; check server log"
                    zs2 = 'you may access the controlpanel but nothing will work until you shutdown the copyparty container and %s config-file (or provide the configuration as command-line arguments)'
                    if self.asrv.is_lxc and len(self.asrv.cfg_files_loaded) == 1:
                        zs2 = zs2 % ("add a",)
                    else:
                        zs2 = zs2 % ("fix the",)

                    html = self.j2s("msg", h1=zs1, h2=zs2)
                    self.reply(html.encode("utf-8", "replace"), 500)
                    return True

                if "ls" in self.uparam:
                    return self.tx_ls_vols()

                if self.vpath:
                    ptn = self.args.nonsus_urls
                    if not ptn or not ptn.search(self.vpath):
                        self.log(t % (self.uname, "/" + self.vpath))

                    return self.tx_404(True)

                self.uparam["h"] = ""

        if "smsg" in self.uparam:
            return self.handle_smsg()

        if "tree" in self.uparam:
            return self.tx_tree()

        if "scan" in self.uparam:
            return self.scanvol()

        if self.args.getmod:
            if "delete" in self.uparam:
                return self.handle_rm([])

            if "move" in self.uparam:
                return self.handle_mv()

            if "copy" in self.uparam:
                return self.handle_cp()

        if not self.vpath and self.ouparam:
            if "reload" in self.uparam:
                return self.handle_reload()

            if "stack" in self.uparam:
                return self.tx_stack()

            if "setck" in self.uparam:
                return self.setck()

            if "reset" in self.uparam:
                return self.set_cfg_reset()

            if "hc" in self.uparam:
                return self.tx_svcs()

            if "shares" in self.uparam:
                return self.tx_shares()

            if "dls" in self.uparam:
                return self.tx_dls()

            if "ru" in self.uparam:
                return self.tx_rups()

            if "idp" in self.uparam:
                return self.tx_idp()

        if "h" in self.uparam:
            return self.tx_mounts()

        if "ups" in self.uparam:
            # vpath is used for share translation
            return self.tx_ups()

        if "rss" in self.uparam:
            return self.tx_rss()

        return self.tx_browser()

    def tx_rss(self) -> bool:
        if self.do_log:
            self.log("RSS  %s @%s" % (self.req, self.uname))

        if not self.can_read:
            return self.tx_404(True)

        vn = self.vn
        if not vn.flags.get("rss"):
            raise Pebkac(405, "RSS is disabled in server config")

        rem = self.rem
        idx = self.conn.get_u2idx()
        if not idx or not hasattr(idx, "p_end"):
            if not HAVE_SQLITE3:
                raise Pebkac(500, "sqlite3 not found on server; rss is disabled")
            raise Pebkac(500, "server busy, cannot generate rss; please retry in a bit")

        uv = [rem]
        if "recursive" in self.uparam:
            uq = "up.rd like ?||'%'"
        else:
            uq = "up.rd == ?"

        zs = str(self.uparam.get("fext", self.args.rss_fext))
        if zs in ("True", "False"):
            zs = ""
        if zs:
            zsl = []
            for ext in zs.split(","):
                zsl.append("+up.fn like '%.'||?")
                uv.append(ext)
            uq += " and ( %s )" % (" or ".join(zsl),)

        zs1 = self.uparam.get("sort") or self.args.rss_sort
        zs2 = zs1.lower()
        zs = RSS_SORT.get(zs2)
        if not zs:
            raise Pebkac(400, "invalid sort key; must be m/u/n/s")

        uq += " order by up." + zs
        if zs1 == zs2:
            uq += " desc"

        nmax = int(self.uparam.get("nf") or self.args.rss_nf)

        hits = idx.run_query(self.uname, [self.vn], uq, uv, False, False, nmax)[0]

        q_pw = a_pw = ""
        pwk = self.args.pw_urlp
        if pwk in self.ouparam and "nopw" not in self.ouparam:
            zs = self.ouparam[pwk]
            q_pw = "?%s=%s" % (pwk, quotep(zs))
            a_pw = "&%s=%s" % (pwk, quotep(zs))
            for i in hits:
                i["rp"] += a_pw if "?" in i["rp"] else q_pw

        title = self.uparam.get("title") or self.vpath.split("/")[-1]
        etitle = html_escape(title, True, True)

        baseurl = "%s://%s/" % (
            "https" if self.is_https else "http",
            self.host,
        )
        feed = baseurl + self.req[1:]
        if pwk in self.ouparam and self.ouparam.get("nopw") == "a":
            feed = re.sub(r"&%s=[^&]*" % (pwk,), "", feed)
        if self.is_vproxied:
            baseurl += self.args.RS
        efeed = html_escape(feed, True, True)
        edirlink = efeed.split("?")[0] + q_pw

        ret = [
            """\


\t
\t\t
\t\t%s
\t\t
\t\t%s
\t\tcopyparty-2
"""
            % (efeed, etitle, edirlink)
        ]

        q = "select fn from cv where rd=? and dn=?"
        crd, cdn = rem.rsplit("/", 1) if "/" in rem else ("", rem)
        try:
            cfn = idx.cur[self.vn.realpath].execute(q, (crd, cdn)).fetchone()[0]
            bos.stat(os.path.join(vn.canonical(rem), cfn))
            cv_url = "%s%s?th=jf%s" % (baseurl, vjoin(self.vpath, cfn), a_pw)
            cv_url = html_escape(cv_url, True, True)
            zs = """\
\t\t
\t\t\t%s
\t\t\t%s
\t\t\t%s
\t\t
"""
            ret.append(zs % (cv_url, etitle, edirlink))
        except:
            pass

        ap = ""
        use_magic = "rmagic" in self.vn.flags

        tpl_t = self.uparam.get("fmt_t") or self.vn.flags["rss_fmt_t"]
        tpl_d = self.uparam.get("fmt_d") or self.vn.flags["rss_fmt_d"]
        kw_t = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_t)]
        kw_d = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_d)]

        for i in hits:
            if use_magic:
                ap = os.path.join(self.vn.realpath, i["rp"])

            tags = i["tags"]
            iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True)
            fname = tags["fname"] = unquotep(i["rp"].split("?")[0].split("/")[-1])
            title = tpl_t
            desc = tpl_d
            for zs1, zs2 in kw_t:
                title = title.replace(zs1, str(tags.get(zs2, "")))
            for zs1, zs2 in kw_d:
                desc = desc.replace(zs1, str(tags.get(zs2, "")))
            title = html_escape(title.strip(), True, True)
            if desc.strip(" -,"):
                desc = html_escape(desc.strip(), True, True)
            else:
                desc = title

            mime = html_escape(guess_mime(fname, ap))
            lmod = formatdate(max(0, i["ts"]))
            zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
            zs = (
                """\
\t\t
\t\t\t%s
\t\t\t%s
\t\t\t%s
\t\t\t%s
\t\t\t%s
\t\t\t
"""
                % zsa
            )
            dur = i["tags"].get(".dur")
            if dur:
                zs += "\t\t\t%d\n" % (dur,)
            ret.append(zs + "\t\t\n")

        ret.append("\t\n\n")
        bret = "".join(ret).encode("utf-8", "replace")
        self.reply(bret, 200, "text/xml; charset=utf-8")
        self.log("rss: %d hits, %d bytes" % (len(hits), len(bret)))
        return True

    def tx_zls(self, abspath) -> bool:
        if self.do_log:
            self.log("zls %s @%s" % (self.req, self.uname))
        if self.args.no_zls:
            raise Pebkac(405, "zip browsing is disabled in server config")

        import zipfile

        try:
            with zipfile.ZipFile(abspath, "r") as zf:
                filelist = [{"fn": f.filename} for f in zf.infolist()]
                ret = json.dumps(filelist).encode("utf-8", "replace")
                self.reply(ret, mime="application/json")
                return True
        except (zipfile.BadZipfile, RuntimeError):
            raise Pebkac(404, "requested file is not a valid zip file")

    def tx_zget(self, abspath) -> bool:
        maxsz = 1024 * 1024 * 64

        inner_path = self.uparam.get("zget")
        if not inner_path:
            raise Pebkac(405, "inner path is required")
        if self.do_log:
            self.log(
                "zget %s \033[35m%s\033[0m @%s" % (self.req, inner_path, self.uname)
            )
        if self.args.no_zls:
            raise Pebkac(405, "zip browsing is disabled in server config")

        import zipfile

        try:
            with zipfile.ZipFile(abspath, "r") as zf:
                zi = zf.getinfo(inner_path)
                if zi.file_size >= maxsz:
                    raise Pebkac(404, "zip bomb defused")
                with zf.open(zi, "r") as fi:
                    mime = guess_mime(inner_path)
                    if mime not in SAFE_MIMES and "nohtml" in self.vn.flags:
                        mime = safe_mime(mime)
                    self.send_headers("oh_f", length=zi.file_size, mime=mime)

                    sendfile_py(
                        self.log,
                        0,
                        zi.file_size,
                        fi,
                        self.s,
                        self.args.s_wr_sz,
                        self.args.s_wr_slp,
                        not self.args.no_poll,
                        {},
                        "",
                    )
        except KeyError:
            raise Pebkac(404, "no such file in archive")
        except (zipfile.BadZipfile, RuntimeError):
            raise Pebkac(404, "requested file is not a valid zip file")
        return True

    def handle_propfind(self) -> bool:
        if self.do_log:
            self.log("PFIND %s @%s" % (self.req, self.uname))
            if "%" in self.req:
                self.log("  `-- %r" % (self.vpath,))

        if self.args.no_dav:
            raise Pebkac(405, "WebDAV is disabled in server config")

        vn = self.vn
        rem = self.rem
        tap = vn.canonical(rem)

        if "davauth" in vn.flags and self.uname == "*":
            raise Pebkac(401, "authenticate")

        from .dxml import parse_xml

        # enc = "windows-31j"
        # enc = "shift_jis"
        enc = "utf-8"
        uenc = enc.upper()
        props = DAV_ALLPROPS

        clen = int(self.headers.get("content-length", 0))
        if clen:
            buf = b""
            for rbuf in self.get_body_reader()[0]:
                buf += rbuf
                if not rbuf or len(buf) >= 32768:
                    break

            sbuf = buf.decode(enc, "replace")
            self.hint = " " + sbuf
            xroot = parse_xml(sbuf)
            xtag = next((x for x in xroot if x.tag.split("}")[-1] == "prop"), None)
            if xtag is not None:
                props = set([y.tag.split("}")[-1] for y in xtag])
            # assume  otherwise; nobody ever gonna 
            self.hint = ""

        zi = int(time.time())
        vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))

        try:
            st = bos.stat(tap)
        except OSError as ex:
            if ex.errno not in (errno.ENOENT, errno.ENOTDIR):
                raise
            if tap:
                raise Pebkac(404)
            st = vst

        topdir = {"vp": "", "st": st}
        fgen: Iterable[dict[str, Any]] = []

        depth = self.headers.get("depth", "infinity").lower()
        if depth == "infinity":
            # allow depth:0 from unmapped root, but require read-axs otherwise
            if not self.can_read and (self.vpath or self.asrv.vfs.realpath):
                t = "depth:infinity requires read-access in %r"
                t = t % ("/" + self.vpath,)
                self.log(t, 3)
                raise Pebkac(401, t)

            if not stat.S_ISDIR(topdir["st"].st_mode):
                t = "depth:infinity can only be used on folders; %r is 0o%o"
                t = t % ("/" + self.vpath, topdir["st"])
                self.log(t, 3)
                raise Pebkac(400, t)

            if not self.args.dav_inf:
                self.log("client wants --dav-inf", 3)
                zb = b'\n'
                self.reply(zb, 403, "application/xml; charset=utf-8")
                return True

            # this will return symlink-target timestamps
            # because lstat=true would not recurse into subfolders
            # and this is a rare case where we actually want that
            fgen = vn.zipgen(
                rem,
                rem,
                set(),
                self.uname,
                True,
                1,
                not self.args.no_scandir,
                wrap=False,
            )

        elif depth == "0" or not stat.S_ISDIR(st.st_mode):
            if depth == "0" and not self.vpath and not vn.realpath:
                # rootless server; give dummy listing
                self.can_read = True
            # propfind on a file; return as topdir
            if not self.can_read and not self.can_get:
                self.log("inaccessible: %r" % ("/" + self.vpath,))
                raise Pebkac(401, "authenticate")

        elif depth == "1":
            _, vfs_ls, vfs_virt = vn.ls(
                rem,
                self.uname,
                not self.args.no_scandir,
                [[True, False]],
                lstat="davrt" not in vn.flags,
                throw=True,
            )
            if not self.can_read:
                vfs_ls = []
            if not self.can_dot:
                vfs_ls = exclude_dotfiles_ls(vfs_ls)
            fgen = [{"vp": vp, "st": st} for vp, st in vfs_ls]

            if vfs_virt:
                zsl = list(vfs_virt)
                if not self.can_dot:
                    zsl = exclude_dotfiles(zsl)
                fgen += [{"vp": v, "st": vst} for v in zsl]

        else:
            t = "invalid depth value '{}' (must be either '0' or '1'{})"
            t2 = " or 'infinity'" if self.args.dav_inf else ""
            raise Pebkac(412, t.format(depth, t2))

        if not self.can_read and not self.can_write and not fgen:
            self.log("inaccessible: %r" % ("/" + self.vpath,))
            raise Pebkac(401, "authenticate")

        zi = (
            vn.flags["du_iwho"]
            if vn.realpath
            and "quota-available-bytes" in props
            and "quotaused" not in props  # macos finder; ingnore it
            else 0
        )
        if zi and (
            zi == 9
            or (zi == 7 and self.uname != "*")
            or (zi == 5 and self.can_write)
            or (zi == 4 and self.can_write and self.can_read)
            or (zi == 3 and self.can_admin)
        ):
            bfree, btot, _ = get_df(vn.realpath, False)
            if btot:
                if "vmaxb" in vn.flags:
                    assert vn.lim  # type: ignore  # !rm
                    btot = vn.lim.vbmax
                    if bfree == vn.lim.c_vb_r:
                        bfree = min(bfree, max(0, vn.lim.vbmax - vn.lim.c_vb_v))
                    else:
                        try:
                            zi, _ = self.conn.hsrv.broker.ask(
                                "up2k.get_volsizes", [vn.realpath]
                            ).get()[0]
                            vn.lim.c_vb_v = zi
                            vn.lim.c_vb_r = bfree
                            bfree = min(bfree, max(0, vn.lim.vbmax - zi))
                        except:
                            pass
                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))

        chunksz = 0x7FF8  # preferred by nginx or cf (dunno which)

        self.send_headers(
            "oh_f",
            None,
            207,
            "text/xml; charset=" + enc,
            {"Transfer-Encoding": "chunked"},
        )

        ap = ""
        use_magic = "rmagic" in vn.flags

        ret = '\n'
        ret = ret.format(uenc)
        for x in fgen:
            rp = vjoin(vtop, x["vp"])
            st: os.stat_result = x["st"]
            mtime = max(0, st.st_mtime)
            if stat.S_ISLNK(st.st_mode):
                try:
                    st = bos.stat(os.path.join(tap, x["vp"]))
                except:
                    continue

            isdir = stat.S_ISDIR(st.st_mode)

            ret += "/%s%s" % (
                quotep(rp),
                "/" if isdir and rp else "",
            )

            pvs: dict[str, str] = {
                "displayname": html_escape(rp.split("/")[-1]),
                "getlastmodified": formatdate(mtime),
                "resourcetype": '' if isdir else "",
                "supportedlock": '',
            }
            if not isdir:
                if use_magic:
                    ap = os.path.join(tap, x["vp"])
                pvs["getcontenttype"] = html_escape(guess_mime(rp, ap))
                pvs["getcontentlength"] = str(st.st_size)
            elif df:
                pvs.update(df)

            for k, v in pvs.items():
                if k not in props:
                    continue
                elif v:
                    ret += "%s" % (k, v, k)
                else:
                    ret += "" % (k,)

            ret += "HTTP/1.1 200 OK"

            missing = ["" % (x,) for x in props if x not in pvs]
            if missing and clen:
                t = "{}HTTP/1.1 404 Not Found"
                ret += t.format("".join(missing))

            ret += ""
            while len(ret) >= chunksz:
                ret = self.send_chunk(ret, enc, chunksz)

        ret += ""
        while ret:
            ret = self.send_chunk(ret, enc, chunksz)

        self.send_chunk("", enc, chunksz)
        # self.reply(ret.encode(enc, "replace"),207, "text/xml; charset=" + enc)
        return True

    def handle_proppatch(self) -> bool:
        if self.do_log:
            self.log("PPATCH %s @%s" % (self.req, self.uname))
            if "%" in self.req:
                self.log("   `-- %r" % (self.vpath,))

        if self.args.no_dav:
            raise Pebkac(405, "WebDAV is disabled in server config")

        if not self.can_write:
            self.log("%s tried to proppatch %r" % (self.uname, "/" + self.vpath))
            raise Pebkac(401, "authenticate")

        from xml.etree import ElementTree as ET

        from .dxml import mkenod, mktnod, parse_xml

        buf = b""
        for rbuf in self.get_body_reader()[0]:
            buf += rbuf
            if not rbuf or len(buf) >= 128 * 1024:
                break

        if self._applesan():
            return True

        txt = buf.decode("ascii", "replace").lower()
        enc = self.get_xml_enc(txt)
        uenc = enc.upper()

        txt = buf.decode(enc, "replace")
        self.hint = " " + txt
        ET.register_namespace("D", "DAV:")
        xroot = mkenod("D:orz")
        xroot.insert(0, parse_xml(txt))
        xprop = xroot.find(r"./{DAV:}propertyupdate/{DAV:}set/{DAV:}prop")
        assert xprop  # !rm
        for ze in xprop:
            ze.clear()
        self.hint = ""

        txt = """HTTP/1.1 403 Forbidden"""
        xroot = parse_xml(txt)

        el = xroot.find(r"./{DAV:}response")
        assert el  # !rm
        e2 = mktnod("D:href", quotep(self.args.SRS + self.vpath))
        el.insert(0, e2)

        el = xroot.find(r"./{DAV:}response/{DAV:}propstat")
        assert el  # !rm
        el.insert(0, xprop)

        ret = '\n'.format(uenc)
        ret += ET.tostring(xroot).decode("utf-8")

        self.reply(ret.encode(enc, "replace"), 207, "text/xml; charset=" + enc)
        return True

    def handle_lock(self) -> bool:
        if self.do_log:
            self.log("LOCK %s @%s" % (self.req, self.uname))
            if "%" in self.req:
                self.log(" `-- %r" % (self.vpath,))

        if self.args.no_dav:
            raise Pebkac(405, "WebDAV is disabled in server config")

        # win7+ deadlocks if we say no; just smile and nod
        if not self.can_write and "Microsoft-WebDAV" not in self.ua:
            self.log("%s tried to lock %r" % (self.uname, "/" + self.vpath))
            raise Pebkac(401, "authenticate")

        from xml.etree import ElementTree as ET

        from .dxml import mkenod, mktnod, parse_xml

        abspath = self.vn.dcanonical(self.rem)

        buf = b""
        for rbuf in self.get_body_reader()[0]:
            buf += rbuf
            if not rbuf or len(buf) >= 128 * 1024:
                break

        if self._applesan():
            return True

        txt = buf.decode("ascii", "replace").lower()
        enc = self.get_xml_enc(txt)
        uenc = enc.upper()

        txt = buf.decode(enc, "replace")
        self.hint = " " + txt
        ET.register_namespace("D", "DAV:")
        lk = parse_xml(txt)
        assert lk.tag == "{DAV:}lockinfo"
        self.hint = ""

        token = str(uuid.uuid4())

        if lk.find(r"./{DAV:}depth") is None:
            depth = self.headers.get("depth", "infinity")
            lk.append(mktnod("D:depth", depth))

        lk.append(mktnod("D:timeout", "Second-3310"))
        lk.append(mkenod("D:locktoken", mktnod("D:href", token)))
        lk.append(
            mkenod("D:lockroot", mktnod("D:href", quotep(self.args.SRS + self.vpath)))
        )

        lk2 = mkenod("D:activelock")
        xroot = mkenod("D:prop", mkenod("D:lockdiscovery", lk2))
        for a in lk:
            lk2.append(a)

        ret = '\n'.format(uenc)
        ret += ET.tostring(xroot).decode("utf-8")

        rc = 200
        if self.can_write and not bos.path.isfile(abspath):
            with open(fsenc(abspath), "wb") as _:
                rc = 201

        self.out_headers["Lock-Token"] = "<{}>".format(token)
        self.reply(ret.encode(enc, "replace"), rc, "text/xml; charset=" + enc)
        return True

    def handle_unlock(self) -> bool:
        if self.do_log:
            self.log("UNLOCK %s @%s" % (self.req, self.uname))
            if "%" in self.req:
                self.log("   `-- %r" % (self.vpath,))

        if self.args.no_dav:
            raise Pebkac(405, "WebDAV is disabled in server config")

        if not self.can_write and "Microsoft-WebDAV" not in self.ua:
            self.log("%s tried to lock %r" % (self.uname, "/" + self.vpath))
            raise Pebkac(401, "authenticate")

        self.send_headers("oh_f", None, 204)
        return True

    def handle_mkcol(self) -> bool:
        if self._applesan():
            return True

        if self.do_log:
            self.log("MKCOL %s @%s" % (self.req, self.uname))
            if "%" in self.req:
                self.log("  `-- %r" % (self.vpath,))

        if self.args.no_dav:
            raise Pebkac(405, "WebDAV is disabled in server config")

        if not self.can_write:
            raise Pebkac(401, "authenticate")

        try:
            return self._mkdir(self.vpath, True)
        except Pebkac as ex:
            if ex.code >= 500:
                raise

            self.reply(b"", ex.code)
            return True

    def handle_cpmv(self) -> bool:
        dst = self.headers["destination"]

        # dolphin (kioworker/6.10) "webdav://127.0.0.1:3923/a/b.txt"
        dst = re.sub("^[a-zA-Z]+://[^/]+", "", dst).lstrip()

        if self.is_vproxied and dst.startswith(self.args.SRS):
            dst = dst[len(self.args.RS) :]

        if self.do_log:
            self.log("%s %s  --//>  %s @%s" % (self.mode, self.req, dst, self.uname))
            if "%" in self.req:
                self.log("  `-- %r" % (self.vpath,))

        if self.args.no_dav:
            raise Pebkac(405, "WebDAV is disabled in server config")

        dst = unquotep(dst)

        # overwrite=True is default; rfc4918 9.8.4
        zs = self.headers.get("overwrite", "").lower()
        overwrite = zs not in ["f", "false"]

        try:
            fun = self._cp if self.mode == "COPY" else self._mv
            return fun(self.vpath, dst.lstrip("/"), overwrite)
        except Pebkac as ex:
            if ex.code == 403:
                ex.code = 401
            raise

    def _applesan(self) -> bool:
        if self.args.dav_mac or "Darwin/" not in self.ua:
            return False

        vp = "/" + self.vpath
        if re.search(APPLESAN_RE, vp):
            zt = '\n{}'
            zb = zt.format(vp).encode("utf-8", "replace")
            self.reply(zb, 423, "text/xml; charset=utf-8")
            return True

        return False

    def send_chunk(self, txt: str, enc: str, bmax: int) -> str:
        orig_len = len(txt)
        buf = txt[:bmax].encode(enc, "replace")[:bmax]
        try:
            _ = buf.decode(enc)
        except UnicodeDecodeError as ude:
            buf = buf[: ude.start]

        txt = txt[len(buf.decode(enc)) :]
        if txt and len(txt) == orig_len:
            raise Pebkac(500, "chunk slicing failed")

        buf = ("%x\r\n" % (len(buf),)).encode(enc) + buf
        self.s.sendall(buf + b"\r\n")
        return txt

    def handle_options(self) -> bool:
        if self.do_log:
            self.log("OPTIONS %s @%s" % (self.req, self.uname))
            if "%" in self.req:
                self.log("    `-- %r" % (self.vpath,))

        oh = self.out_headers
        oh["Allow"] = ", ".join(self.conn.hsrv.mallow)

        if not self.args.no_dav:
            # PROPPATCH, LOCK, UNLOCK, COPY: noop (spec-must)
            oh["Dav"] = "1, 2"
            oh["Ms-Author-Via"] = "DAV"

        # winxp-webdav doesnt know what 204 is
        self.send_headers("oh_f", 0, 200)
        return True

    def handle_delete(self) -> bool:
        self.log("DELETE %s @%s" % (self.req, self.uname))
        if "%" in self.req:
            self.log("   `-- %r" % (self.vpath,))
        return self.handle_rm([])

    def handle_put(self) -> bool:
        self.log("PUT  %s @%s" % (self.req, self.uname))
        if "%" in self.req:
            self.log(" `-- %r" % (self.vpath,))

        if not self.can_write:
            t = "user %s does not have write-access under /%s"
            raise Pebkac(403 if self.pw else 401, t % (self.uname, self.vn.vpath))

        if not self.args.no_dav and self._applesan():
            return False

        if self.headers.get("expect", "").lower() == "100-continue":
            try:
                self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
            except:
                raise Pebkac(400, "client d/c before 100 continue")

        return self.handle_stash(True)

    def handle_post(self) -> bool:
        self.log("POST %s @%s" % (self.req, self.uname))
        if "%" in self.req:
            self.log(" `-- %r" % (self.vpath,))

        if self.headers.get("expect", "").lower() == "100-continue":
            try:
                self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
            except:
                raise Pebkac(400, "client d/c before 100 continue")

        if "raw" in self.uparam:
            return self.handle_stash(False)

        ctype = self.headers.get("content-type", "").lower()

        if "multipart/form-data" in ctype:
            return self.handle_post_multipart()

        if (
            "application/json" in ctype
            or "text/plain" in ctype
            or "application/xml" in ctype
        ):
            return self.handle_post_json()

        if "smsg" in self.uparam:
            return self.handle_smsg()

        if "move" in self.uparam:
            return self.handle_mv()

        if "copy" in self.uparam:
            return self.handle_cp()

        if "delete" in self.uparam:
            return self.handle_rm([])

        if "eshare" in self.uparam:
            return self.handle_eshare()

        if "fs_abrt" in self.uparam:
            return self.handle_fs_abrt()

        if "application/octet-stream" in ctype:
            return self.handle_post_binary()

        if "application/x-www-form-urlencoded" in ctype:
            opt = self.args.urlform
            if "stash" in opt:
                return self.handle_stash(False)

            xm = []
            xm_rsp = {}

            if "save" in opt:
                post_sz, _, _, _, _, path, _ = self.dump_to_file(False)
                self.log("urlform: %d bytes, %r" % (post_sz, path))
            elif "print" in opt:
                reader, _ = self.get_body_reader()
                buf = b""
                for rbuf in reader:
                    buf += rbuf
                    if not rbuf or len(buf) >= 32768:
                        break

                if buf:
                    orig = buf.decode("utf-8", "replace")
                    t = "urlform_raw %d @ %r\n  %r\n"
                    self.log(t % (len(orig), "/" + self.vpath, orig))
                    try:
                        zb = unquote(buf.replace(b"+", b" ").replace(b"&", b"\n"))
                        plain = zb.decode("utf-8", "replace")
                        if buf.startswith(b"msg="):
                            plain = plain[4:]
                            xm = self.vn.flags.get("xm")
                            if xm:
                                xm_rsp = runhook(
                                    self.log,
                                    self.conn.hsrv.broker,
                                    None,
                                    "xm",
                                    xm,
                                    self.vn.canonical(self.rem),
                                    self.vpath,
                                    self.host,
                                    self.uname,
                                    self.asrv.vfs.get_perms(self.vpath, self.uname),
                                    time.time(),
                                    len(buf),
                                    self.ip,
                                    time.time(),
                                    [plain, orig],
                                )

                        t = "urlform_dec %d @ %r\n  %r\n"
                        self.log(t % (len(plain), "/" + self.vpath, plain))

                    except Exception as ex:
                        self.log(repr(ex))

            if "xm" in opt:
                if xm:
                    self.loud_reply(xm_rsp.get("stdout") or "", status=202)
                    return True
                else:
                    return self.handle_get()

            if "get" in opt:
                return self.handle_get()

            raise Pebkac(405, "POST(%r) is disabled in server config" % (ctype,))

        raise Pebkac(405, "don't know how to handle POST(%r)" % (ctype,))

    def handle_smsg(self) -> bool:
        if self.mode not in self.args.smsg_set:
            raise Pebkac(403, "smsg is disabled for this http-method in server config")

        msg = self.uparam["smsg"]
        self.log("smsg %d @ %r\n  %r\n" % (len(msg), "/" + self.vpath, msg))

        xm = self.vn.flags.get("xm")
        if xm:
            xm_rsp = runhook(
                self.log,
                self.conn.hsrv.broker,
                None,
                "xm",
                xm,
                self.vn.canonical(self.rem),
                self.vpath,
                self.host,
                self.uname,
                self.asrv.vfs.get_perms(self.vpath, self.uname),
                time.time(),
                len(msg),
                self.ip,
                time.time(),
                [msg, msg],
            )
            self.loud_reply(xm_rsp.get("stdout") or "", status=202)
        else:
            self.loud_reply("k", status=202)
        return True

    def get_xml_enc(self, txt: str) -> str:
        ofs = txt[:512].find(' encoding="')
        enc = ""
        if ofs + 1:
            enc = txt[ofs + 6 :].split('"')[1]
        else:
            enc = self.headers.get("content-type", "").lower()
            ofs = enc.find("charset=")
            if ofs + 1:
                enc = enc[ofs + 4].split("=")[1].split(";")[0].strip("\"'")
            else:
                enc = ""

        return enc or "utf-8"

    def get_body_reader(self) -> tuple[Generator[bytes, None, None], int]:
        bufsz = self.args.s_rd_sz
        if "chunked" in self.headers.get("transfer-encoding", "").lower():
            return read_socket_chunked(self.sr, bufsz), -1

        remains = int(self.headers.get("content-length", -1))
        if remains == -1:
            self.keepalive = False
            self.in_hdr_recv = True
            self.s.settimeout(max(self.args.s_tbody // 20, 1))
            return read_socket_unbounded(self.sr, bufsz), remains
        else:
            return read_socket(self.sr, bufsz, remains), remains

    def dump_to_file(self, is_put: bool) -> tuple[int, str, str, str, int, str, str]:
        # post_sz, halg, sha_hex, sha_b64, remains, path, url
        reader, remains = self.get_body_reader()
        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
        rnd, lifetime, xbu, xau = self.upload_flags(vfs)
        lim = vfs.get_dbv(rem)[0].lim
        fdir = vfs.canonical(rem)
        fn = None
        if rem and not self.trailing_slash and not bos.path.isdir(fdir):
            fdir, fn = os.path.split(fdir)
            rem, _ = vsplit(rem)

        if lim:
            fdir, rem = lim.all(
                self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
            )

        bos.makedirs(fdir, vf=vfs.flags)

        open_ka: dict[str, Any] = {"fun": open}
        open_a = ["wb", self.args.iobuf]

        # user-request || config-force
        if ("gz" in vfs.flags or "xz" in vfs.flags) and (
            "pk" in vfs.flags
            or "pk" in self.uparam
            or "gz" in self.uparam
            or "xz" in self.uparam
        ):
            fb = {"gz": 9, "xz": 0}  # default/fallback level
            lv = {}  # selected level
            alg = ""  # selected algo (gz=preferred)

            # user-prefs first
            if "gz" in self.uparam or "pk" in self.uparam:  # def.pk
                alg = "gz"
            if "xz" in self.uparam:
                alg = "xz"
            if alg:
                zso = self.uparam.get(alg)
                lv[alg] = fb[alg] if zso is None else int(zso)

            if alg not in vfs.flags:
                alg = "gz" if "gz" in vfs.flags else "xz"

            # then server overrides
            pk = vfs.flags.get("pk")
            if pk is not None:
                # config-forced on
                alg = alg or "gz"  # def.pk
                try:
                    # config-forced opts
                    alg, nlv = pk.split(",")
                    lv[alg] = int(nlv)
                except:
                    pass

            lv[alg] = lv.get(alg) or fb.get(alg) or 0

            self.log("compressing with {} level {}".format(alg, lv.get(alg)))
            if alg == "gz":
                open_ka["fun"] = gzip.GzipFile
                open_a = ["wb", lv[alg], None, 0x5FEE6600]  # 2021-01-01
            elif alg == "xz":
                assert lzma  # type: ignore  # !rm
                open_ka = {"fun": lzma.open, "preset": lv[alg]}
                open_a = ["wb"]
            else:
                self.log("fallthrough? thats a bug", 1)

        suffix = "-{:.6f}-{}".format(time.time(), self.dip())
        nameless = not fn
        if nameless:
            fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())

        params = {"suffix": suffix, "fdir": fdir, "vf": vfs.flags}
        if self.args.nw:
            params = {}
            fn = os.devnull

        params.update(open_ka)
        assert fn  # !rm

        if not self.args.nw:
            if rnd:
                fn = rand_name(fdir, fn, rnd)

            fn = sanitize_fn(fn or "")

        path = os.path.join(fdir, fn)

        if xbu:
            at = time.time() - lifetime
            vp = vjoin(self.vpath, fn) if nameless else self.vpath
            hr = runhook(
                self.log,
                self.conn.hsrv.broker,
                None,
                "xbu.http.dump",
                xbu,
                path,
                vp,
                self.host,
                self.uname,
                self.asrv.vfs.get_perms(self.vpath, self.uname),
                at,
                remains,
                self.ip,
                at,
                None,
            )
            t = hr.get("rejectmsg") or ""
            if t or hr.get("rc") != 0:
                if not t:
                    t = "upload blocked by xbu server config: %r" % (vp,)
                self.log(t, 1)
                raise Pebkac(403, t)
            if hr.get("reloc"):
                x = pathmod(self.asrv.vfs, path, vp, hr["reloc"])
                if x:
                    if self.args.hook_v:
                        log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
                    fdir, self.vpath, fn, (vfs, rem) = x
                    if self.args.nw:
                        fn = os.devnull
                    else:
                        bos.makedirs(fdir, vf=vfs.flags)
                        path = os.path.join(fdir, fn)
                        if not nameless:
                            self.vpath = vjoin(self.vpath, fn)
                        params["fdir"] = fdir

        if (
            is_put
            and not (self.args.no_dav or self.args.nw)
            and "append" not in self.uparam
            and bos.path.exists(path)
        ):
            # allow overwrite if...
            #  * volflag 'daw' is set, or client is definitely webdav
            #  * and account has delete-access
            # or...
            #  * file exists, is empty, sufficiently new
            #  * and there is no .PARTIAL

            tnam = fn + ".PARTIAL"
            if self.args.dotpart:
                tnam = "." + tnam

            if (
                self.can_delete
                and (
                    vfs.flags.get("daw")
                    or "replace" in self.headers
                    or "x-oc-mtime" in self.headers
                    or (
                        self.args.dav_port
                        and self.args.dav_port == self.s.getsockname()[1]
                    )
                )
            ) or (
                not bos.path.exists(os.path.join(fdir, tnam))
                and not bos.path.getsize(path)
                and bos.path.getmtime(path) >= time.time() - self.args.blank_wt
            ):
                # small toctou, but better than clobbering a hardlink
                wunlink(self.log, path, vfs.flags)

        hasher = None
        copier = hashcopy
        halg = self.ouparam.get("ck") or self.headers.get("ck") or vfs.flags["put_ck"]
        if halg == "sha512":
            pass
        elif halg == "no":
            copier = justcopy
            halg = ""
        elif halg == "md5":
            hasher = hashlib.md5(**USED4SEC)
        elif halg == "sha1":
            hasher = hashlib.sha1(**USED4SEC)
        elif halg == "sha256":
            hasher = hashlib.sha256(**USED4SEC)
        elif halg in ("blake2", "b2"):
            hasher = hashlib.blake2b(**USED4SEC)
        elif halg in ("blake2s", "b2s"):
            hasher = hashlib.blake2s(**USED4SEC)
        else:
            raise Pebkac(500, "unknown hash alg")

        if "apnd" in self.uparam and not self.args.nw and bos.path.exists(path):
            zs = vfs.flags["apnd_who"]
            if (
                zs == "w"
                or (zs == "aw" and self.can_admin)
                or (zs == "dw" and self.can_delete)
            ):
                pass
            elif zs == "ndd":
                raise Pebkac(400, "append is denied here due to non-reflink dedup")
            else:
                raise Pebkac(400, "you do not have permission to append")
            zs = os.path.join(params["fdir"], fn)
            self.log("upload will append to [%s]" % (zs,))
            f = open(zs, "ab")
        else:
            f, fn = ren_open(fn, *open_a, **params)

        max_sz = 0
        if lim and remains < 0:
            if lim.vbmax:
                max_sz = lim.c_vb_v
            if lim.smax and (not max_sz or max_sz > lim.smax):
                max_sz = lim.smax

        try:
            path = os.path.join(fdir, fn)
            post_sz, sha_hex, sha_b64 = copier(
                reader, f, hasher, max_sz, self.args.s_wr_slp
            )
        except:
            if max_sz and self.sr.nb >= max_sz:
                f.close()  # windows
                wunlink(self.log, path, vfs.flags)
            raise
        finally:
            f.close()

        if lim:
            lim.nup(self.ip)
            lim.bup(self.ip, post_sz)
            try:
                lim.chk_sz(post_sz)
                lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, post_sz)
            except:
                wunlink(self.log, path, vfs.flags)
                raise

        if self.args.nw:
            return post_sz, halg, sha_hex, sha_b64, remains, path, ""

        at = mt = time.time() - lifetime
        cli_mt = self.headers.get("x-oc-mtime")
        if cli_mt:
            bos.utime_c(self.log, path, float(cli_mt), False)

        if nameless and "magic" in vfs.flags:
            try:
                ext = self.conn.hsrv.magician.ext(path)
            except Exception as ex:
                self.log("filetype detection failed for %r: %s" % (path, ex), 6)
                ext = None

            if ext:
                if rnd:
                    fn2 = rand_name(fdir, "a." + ext, rnd)
                else:
                    fn2 = fn.rsplit(".", 1)[0] + "." + ext

                params["suffix"] = suffix[:-4]
                f, fn2 = ren_open(fn2, *open_a, **params)
                f.close()

                path2 = os.path.join(fdir, fn2)
                atomic_move(self.log, path, path2, vfs.flags)
                fn = fn2
                path = path2

        if xau:
            vp = vjoin(self.vpath, fn) if nameless else self.vpath
            hr = runhook(
                self.log,
                self.conn.hsrv.broker,
                None,
                "xau.http.dump",
                xau,
                path,
                vp,
                self.host,
                self.uname,
                self.asrv.vfs.get_perms(self.vpath, self.uname),
                mt,
                post_sz,
                self.ip,
                at,
                None,
            )
            t = hr.get("rejectmsg") or ""
            if t or hr.get("rc") != 0:
                if not t:
                    t = "upload blocked by xau server config: %r" % (vp,)
                self.log(t, 1)
                wunlink(self.log, path, vfs.flags)
                raise Pebkac(403, t)
            if hr.get("reloc"):
                x = pathmod(self.asrv.vfs, path, vp, hr["reloc"])
                if x:
                    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, vf=vfs.flags)
                    path2 = os.path.join(fdir, fn)
                    atomic_move(self.log, path, path2, vfs.flags)
                    path = path2
                    if not nameless:
                        self.vpath = vjoin(self.vpath, fn)
            sz = bos.path.getsize(path)
        else:
            sz = post_sz

        vfs, rem = vfs.get_dbv(rem)
        self.conn.hsrv.broker.say(
            "up2k.hash_file",
            vfs.realpath,
            vfs.vpath,
            vfs.flags,
            rem,
            fn,
            self.ip,
            at,
            self.uname,
            True,
        )

        vsuf = ""
        if (self.can_read or self.can_upget) and "fk" in vfs.flags:
            alg = 2 if "fka" in vfs.flags else 1
            vsuf = "?k=" + self.gen_fk(
                alg,
                self.args.fk_salt,
                path,
                sz,
                0 if ANYWIN else bos.stat(path).st_ino,
            )[: vfs.flags["fk"]]

        if "media" in self.uparam or "medialinks" in vfs.flags:
            vsuf += "&v" if vsuf else "?v"

        vpath = "/".join([x for x in [vfs.vpath, rem, fn] if x])
        vpath = quotep(vpath)

        if self.args.up_site:
            url = "%s%s%s%s" % (
                self.args.up_site,
                self.args.RS,
                vpath,
                vsuf,
            )
        else:
            url = "%s://%s/%s%s%s" % (
                "https" if self.is_https else "http",
                self.host,
                self.args.RS,
                vpath,
                vsuf,
            )

        return post_sz, halg, sha_hex, sha_b64, remains, path, url

    def handle_stash(self, is_put: bool) -> bool:
        post_sz, halg, sha_hex, sha_b64, remains, path, url = self.dump_to_file(is_put)
        spd = self._spd(post_sz)
        t = "%s wrote %d/%d bytes to %r  # %s"
        self.log(t % (spd, post_sz, remains, path, sha_b64[:28]))  # 21

        mime = "text/plain; charset=utf-8"
        ac = self.uparam.get("want") or self.headers.get("accept") or ""
        if ac:
            ac = ac.split(";", 1)[0].lower()
            if ac == "application/json":
                ac = "json"
        if ac == "url":
            t = url
        elif ac == "json" or "j" in self.uparam:
            jmsg = {"fileurl": url, "filesz": post_sz}
            if halg:
                jmsg[halg] = sha_hex[:56]
                jmsg["sha_b64"] = sha_b64

            mime = "application/json"
            t = json.dumps(jmsg, indent=2, sort_keys=True)
        else:
            t = "{}\n{}\n{}\n{}\n".format(post_sz, sha_b64, sha_hex[:56], url)

        h = {"Location": url} if is_put and url else {}

        if "x-oc-mtime" in self.headers:
            h["X-OC-MTime"] = "accepted"
            t = ""  # some webdav clients expect/prefer this

        self.reply(t.encode("utf-8", "replace"), 201, mime=mime, headers=h)
        return True

    def bakflip(
        self,
        f: typing.BinaryIO,
        ap: str,
        ofs: int,
        sz: int,
        good_sha: str,
        bad_sha: str,
        flags: dict[str, Any],
    ) -> None:
        now = time.time()
        t = "bad-chunk:  %.3f  %s  %s  %d  %s  %s  %r"
        t = t % (now, bad_sha, good_sha, ofs, self.ip, self.uname, ap)
        self.log(t, 5)

        if self.args.bf_log:
            try:
                with open(self.args.bf_log, "ab+") as f2:
                    f2.write((t + "\n").encode("utf-8", "replace"))
            except Exception as ex:
                self.log("append %s failed: %r" % (self.args.bf_log, ex))

        if not self.args.bak_flips or self.args.nw:
            return

        sdir = self.args.bf_dir
        fp = os.path.join(sdir, bad_sha)
        if bos.path.exists(fp):
            return self.log("no bakflip; have it", 6)

        if not bos.path.isdir(sdir):
            bos.makedirs(sdir)

        if len(bos.listdir(sdir)) >= self.args.bf_nc:
            return self.log("no bakflip; too many", 3)

        nrem = sz
        f.seek(ofs)
        with open(fp, "wb") as fo:
            while nrem:
                buf = f.read(min(nrem, self.args.iobuf))
                if not buf:
                    break

                nrem -= len(buf)
                fo.write(buf)

        if nrem:
            self.log("bakflip truncated; {} remains".format(nrem), 1)
            atomic_move(self.log, fp, fp + ".trunc", flags)
        else:
            self.log("bakflip ok", 2)

    def _spd(self, nbytes: int, add: bool = True) -> str:
        if add:
            self.conn.nbyte += nbytes

        spd1 = get_spd(nbytes, self.t0)
        spd2 = get_spd(self.conn.nbyte, self.conn.t0)
        return "%s %s n%s" % (spd1, spd2, self.conn.nreq)

    def handle_post_multipart(self) -> bool:
        self.parser = MultipartParser(self.log, self.args, self.sr, self.headers)
        self.parser.parse()

        file0: list[tuple[str, Optional[str], Generator[bytes, None, None]]] = []
        try:
            act = self.parser.require("act", 64)
        except WrongPostKey as ex:
            if ex.got == "f" and ex.fname:
                self.log("missing 'act', but looks like an upload so assuming that")
                file0 = [(ex.got, ex.fname, ex.datagen)]
                act = "bput"
            else:
                raise

        if act == "login":
            return self.handle_login()

        if act == "mkdir":
            return self.handle_mkdir()

        if act == "new_md":
            # kinda silly but has the least side effects
            return self.handle_new_md()

        if act in ("bput", "uput"):
            return self.handle_plain_upload(file0, act == "uput")

        if act == "tput":
            return self.handle_text_upload()

        if act == "zip":
            return self.handle_zip_post()

        if act == "chpw":
            return self.handle_chpw()

        if act == "logout":
            return self.handle_logout()

        raise Pebkac(422, "invalid action %r" % (act,))

    def handle_zip_post(self) -> bool:
        assert self.parser  # !rm
        try:
            k = next(x for x in self.uparam if x in ("zip", "tar"))
        except:
            raise Pebkac(422, "need zip or tar keyword")

        v = self.uparam[k]

        if self._use_dirkey(self.vn, ""):
            vn = self.vn
            rem = self.rem
        else:
            vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False)

        zs = self.parser.require("files", 1024 * 1024)
        if not zs:
            raise Pebkac(422, "need files list")

        items = zs.replace("\r", "").split("\n")
        items = [unquotep(x) for x in items if items]

        self.parser.drop()
        return self.tx_zip(k, v, "", vn, rem, items)

    def handle_post_json(self) -> bool:
        try:
            remains = int(self.headers["content-length"])
        except:
            raise Pebkac(411)

        if remains > 1024 * 1024:
            raise Pebkac(413, "json 2big")

        enc = "utf-8"
        ctype = self.headers.get("content-type", "").lower()
        if "charset" in ctype:
            enc = ctype.split("charset")[1].strip(" =").split(";")[0].strip()

        try:
            json_buf = self.sr.recv_ex(remains)
        except UnrecvEOF:
            raise Pebkac(422, "client disconnected while posting JSON")

        try:
            body = json.loads(json_buf.decode(enc, "replace"))
            try:
                zds = {k: v for k, v in body.items()}
                zds["hash"] = "%d chunks" % (len(body["hash"]),)
            except:
                zds = body
            t = "POST len=%d type=%s ip=%s user=%s req=%r json=%s"
            self.log(t % (len(json_buf), enc, self.ip, self.uname, self.req, zds))
        except:
            raise Pebkac(422, "you POSTed %d bytes of invalid json" % (len(json_buf),))

        # self.reply(b"cloudflare", 503)
        # return True

        if "srch" in self.uparam or "srch" in body:
            return self.handle_search(body)

        if "share" in self.uparam:
            return self.handle_share(body)

        if "delete" in self.uparam:
            return self.handle_rm(body)

        name = undot(body["name"])
        if "/" in name:
            raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")

        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
        fsnt = vfs.flags["fsnt"]
        if fsnt != "lin":
            tl = VPTL_WIN if fsnt == "win" else VPTL_MAC
            rem = rem.translate(tl)
            name = name.translate(tl)
        dbv, vrem = vfs.get_dbv(rem)

        name = sanitize_fn(name)
        if (
            not self.can_read
            and self.can_write
            and name.lower() in dbv.flags["emb_all"]
            and "wo_up_readme" not in dbv.flags
        ):
            name = "_wo_" + name

        body["name"] = name
        body["vtop"] = dbv.vpath
        body["ptop"] = dbv.realpath
        body["prel"] = vrem
        body["host"] = self.host
        body["user"] = self.uname
        body["addr"] = self.ip
        body["vcfg"] = dbv.flags

        if not self.can_delete and not body.get("replace") == "skip":
            body.pop("replace", None)

        if rem:
            dst = vfs.canonical(rem)
            try:
                if not bos.path.isdir(dst):
                    bos.makedirs(dst, vf=vfs.flags)
            except OSError as ex:
                self.log("makedirs failed %r" % (dst,))
                if not bos.path.isdir(dst):
                    if ex.errno == errno.EACCES:
                        raise Pebkac(500, "the server OS denied write-access")

                    if ex.errno == errno.EEXIST:
                        raise Pebkac(400, "some file got your folder name")

                    raise Pebkac(500, min_ex())
            except:
                raise Pebkac(500, min_ex())

        # not to protect u2fh, but to prevent handshakes while files are closing
        with self.u2mutex:
            x = self.conn.hsrv.broker.ask("up2k.handle_json", body, self.u2fh.aps)
            ret = x.get()

        if self.args.shr and self.vpath.startswith(self.args.shr1):
            # strip common suffix (uploader's folder structure)
            vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
            if not ret["purl"].startswith(vp_vfs):
                t = "share-mapping failed; req=%r dbv=%r vrem=%r n1=%r n2=%r purl=%r"
                zt = (self.vpath, dbv.vpath, vrem, vp_req, vp_vfs, ret["purl"])
                raise Pebkac(500, t % zt)
            ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]

        if self.is_vproxied:
            if "purl" in ret:
                ret["purl"] = self.args.SR + ret["purl"]

        ret = json.dumps(ret)
        self.log(ret)
        self.reply(ret.encode("utf-8"), mime="application/json")
        return True

    def handle_search(self, body: dict[str, Any]) -> bool:
        idx = self.conn.get_u2idx()
        if not idx or not hasattr(idx, "p_end"):
            if not HAVE_SQLITE3:
                raise Pebkac(500, "sqlite3 not found on server; search is disabled")
            raise Pebkac(500, "server busy, cannot search; please retry in a bit")

        vols: list[VFS] = []
        seen: dict[VFS, bool] = {}
        for vtop in self.rvol:
            vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False)
            vfs = vfs.dbv or vfs
            if vfs in seen:
                continue

            seen[vfs] = True
            vols.append(vfs)

        t0 = time.time()
        if idx.p_end:
            penalty = 0.7
            t_idle = t0 - idx.p_end
            if idx.p_dur > 0.7 and t_idle < penalty:
                t = "rate-limit {:.1f} sec, cost {:.2f}, idle {:.2f}"
                raise Pebkac(429, t.format(penalty, idx.p_dur, t_idle))

        if "srch" in body:
            # search by up2k hashlist
            vbody = copy.deepcopy(body)
            vbody["hash"] = len(vbody["hash"])
            self.log("qj: " + repr(vbody))
            hits = idx.fsearch(self.uname, vols, body)
            msg: Any = repr(hits)
            taglist: list[str] = []
            trunc = False
        else:
            # search by query params
            q = body["q"]
            n = body.get("n", self.args.srch_hits)
            self.log("qj: %r |%d|" % (q, n))
            hits, taglist, trunc = idx.search(self.uname, vols, q, n)
            msg = len(hits)

        idx.p_end = time.time()
        idx.p_dur = idx.p_end - t0
        self.log("q#: %r (%.2fs)" % (msg, idx.p_dur))

        order = []
        for t in self.args.mte:
            if t in taglist:
                order.append(t)
        for t in taglist:
            if t not in order:
                order.append(t)

        if self.is_vproxied:
            for hit in hits:
                hit["rp"] = self.args.RS + hit["rp"]

        rj = {"hits": hits, "tag_order": order, "trunc": trunc}
        r = json.dumps(rj).encode("utf-8")
        self.reply(r, mime="application/json")
        return True

    def handle_post_binary(self) -> bool:
        try:
            postsize = remains = int(self.headers["content-length"])
        except:
            raise Pebkac(400, "you must supply a content-length for binary POST")

        try:
            chashes = self.headers["x-up2k-hash"].split(",")
            wark = self.headers["x-up2k-wark"]
        except KeyError:
            raise Pebkac(400, "need hash and wark headers for binary POST")

        chashes = [x.strip() for x in chashes]
        if len(chashes) == 3 and len(chashes[1]) == 1:
            # the first hash, then length of consecutive hashes,
            # then a list of stitched hashes as one long string
            clen = int(chashes[1])
            siblings = chashes[2]
            chashes = [chashes[0]]
            for n in range(0, len(siblings), clen):
                chashes.append(siblings[n : n + clen])

        vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True)
        ptop = vfs.get_dbv("")[0].realpath
        # if this is a share, then get_dbv has been overridden to return
        # the dbv (which does not exist as a property). And its realpath
        # could point into the middle of its origin vfs node, meaning it
        # is not necessarily registered with up2k, so get_dbv is crucial

        broker = self.conn.hsrv.broker
        x = broker.ask("up2k.handle_chunks", ptop, wark, chashes)
        response = x.get()
        chashes, chunksize, cstarts, path, lastmod, fsize, sprs = response
        maxsize = chunksize * len(chashes)
        cstart0 = cstarts[0]
        locked = chashes  # remaining chunks to be received in this request
        written = []  # chunks written to disk, but not yet released by up2k
        num_left = -1  # num chunks left according to most recent up2k release
        bail1 = False  # used in sad path to avoid contradicting error-text
        treport = time.time()  # ratelimit up2k reporting to reduce overhead

        try:
            if "x-up2k-subc" in self.headers:
                sc_ofs = int(self.headers["x-up2k-subc"])
                chash = chashes[0]

                u2sc = self.conn.hsrv.u2sc
                try:
                    sc_pofs, hasher = u2sc[chash]
                    if not sc_ofs:
                        t = "client restarted the chunk; forgetting subchunk offset %d"
                        self.log(t % (sc_pofs,))
                        raise Exception()
                except:
                    sc_pofs = 0
                    hasher = hashlib.sha512()

                et = "subchunk protocol error; resetting chunk "
                if sc_pofs != sc_ofs:
                    u2sc.pop(chash, None)
                    t = "%s[%s]: the expected resume-point was %d, not %d"
                    raise Pebkac(400, t % (et, chash, sc_pofs, sc_ofs))
                if len(cstarts) > 1:
                    u2sc.pop(chash, None)
                    t = "%s[%s]: only a single subchunk can be uploaded in one request; you are sending %d chunks"
                    raise Pebkac(400, t % (et, chash, len(cstarts)))
                csize = min(chunksize, fsize - cstart0[0])
                cstart0[0] += sc_ofs  # also sets cstarts[0][0]
                sc_next_ofs = sc_ofs + postsize
                if sc_next_ofs > csize:
                    u2sc.pop(chash, None)
                    t = "%s[%s]: subchunk offset (%d) plus postsize (%d) exceeds chunksize (%d)"
                    raise Pebkac(400, t % (et, chash, sc_ofs, postsize, csize))
                else:
                    final_subchunk = sc_next_ofs == csize
                    t = "subchunk %s %d:%d/%d %s"
                    zs = "END" if final_subchunk else ""
                    self.log(t % (chash[:15], sc_ofs, sc_next_ofs, csize, zs), 6)
                    if final_subchunk:
                        u2sc.pop(chash, None)
                    else:
                        u2sc[chash] = (sc_next_ofs, hasher)
            else:
                hasher = None
                final_subchunk = True

            if self.args.nw:
                path = os.devnull

            if remains > maxsize:
                t = "your client is sending %d bytes which is too much (server expected %d bytes at most)"
                raise Pebkac(400, t % (remains, maxsize))

            t = "writing %r %s+%d #%d+%d %s"
            chunkno = cstart0[0] // chunksize
            zs = " ".join([chashes[0][:15]] + [x[:9] for x in chashes[1:]])
            self.log(t % (path, cstart0, remains, chunkno, len(chashes), zs))

            f = None
            fpool = not self.args.no_fpool and sprs
            if fpool:
                with self.u2mutex:
                    try:
                        f = self.u2fh.pop(path)
                    except:
                        pass

            f = f or open(fsenc(path), "rb+", self.args.iobuf)

            try:
                for chash, cstart in zip(chashes, cstarts):
                    f.seek(cstart[0])
                    reader = read_socket(
                        self.sr, self.args.s_rd_sz, min(remains, chunksize)
                    )
                    post_sz, _, sha_b64 = hashcopy(
                        reader, f, hasher, 0, self.args.s_wr_slp
                    )

                    if sha_b64 != chash and final_subchunk:
                        try:
                            self.bakflip(
                                f, path, cstart[0], post_sz, chash, sha_b64, vfs.flags
                            )
                        except:
                            self.log("bakflip failed: " + min_ex())

                        t = "your chunk got corrupted somehow (received {} bytes); expected vs received hash:\n{}\n{}"
                        raise Pebkac(400, t.format(post_sz, chash, sha_b64))

                    remains -= post_sz

                    if len(cstart) > 1 and path != os.devnull:
                        t = " & ".join(unicode(x) for x in cstart[1:])
                        self.log("clone %s to %s" % (cstart[0], t))
                        ofs = 0
                        while ofs < chunksize:
                            bufsz = max(4 * 1024 * 1024, self.args.iobuf)
                            bufsz = min(chunksize - ofs, bufsz)
                            f.seek(cstart[0] + ofs)
                            buf = f.read(bufsz)
                            for wofs in cstart[1:]:
                                f.seek(wofs + ofs)
                                f.write(buf)

                            ofs += len(buf)

                        self.log("clone {} done".format(cstart[0]))

                    # be quick to keep the tcp winsize scale;
                    # if we can't confirm rn then that's fine
                    if final_subchunk:
                        written.append(chash)
                    now = time.time()
                    if now - treport < 1:
                        continue
                    treport = now
                    x = broker.ask(
                        "up2k.fast_confirm_chunks", ptop, wark, written, locked
                    )
                    num_left, t = x.get()
                    if num_left < -1:
                        self.loud_reply(t, status=500)
                        locked = written = []
                        return False
                    elif num_left >= 0:
                        t = "got %d more chunks, %d left"
                        self.log(t % (len(written), num_left), 6)
                        locked = locked[len(written) :]
                        written = []

                if not fpool:
                    f.close()
                else:
                    with self.u2mutex:
                        self.u2fh.put(path, f)
            except:
                # maybe busted handle (eg. disk went full)
                f.close()
                raise
        finally:
            if locked:
                # now block until all chunks released+confirmed
                x = broker.ask("up2k.confirm_chunks", ptop, wark, written, locked)
                num_left, t = x.get()
                if num_left < 0:
                    self.loud_reply(t, status=500)
                    bail1 = True
                else:
                    t = "got %d more chunks, %d left"
                    self.log(t % (len(written), num_left), 6)

        if num_left < 0:
            if bail1:
                return False
            raise Pebkac(500, "unconfirmed; see fileserver log")

        if not num_left and fpool:
            with self.u2mutex:
                self.u2fh.close(path)

        if not num_left and not self.args.nw:
            broker.ask("up2k.finish_upload", ptop, wark, self.u2fh.aps).get()

        cinf = self.headers.get("x-up2k-stat", "")

        spd = self._spd(postsize)
        self.log("%70s thank %r" % (spd, cinf))

        if remains:
            t = "incorrect content-length from client"
            self.log("%s; header=%d, remains=%d" % (t, postsize, remains), 3)
            raise Pebkac(400, t)

        self.reply(b"thank")
        return True

    def handle_chpw(self) -> bool:
        assert self.parser  # !rm
        if self.args.usernames:
            self.parser.require("uname", 64)
        pwd = self.parser.require("pw", 64)
        self.parser.drop()

        ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
        if ok:
            self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
            if self.args.usernames:
                pwd = "%s:%s" % (self.uname, pwd)
            ok, msg = self.get_pwd_cookie(pwd)
            if ok:
                msg = "new password OK"

        redir = (self.args.SRS + "?h") if ok else ""
        h2 = 'continue'
        html = self.j2s("msg", h1=msg, h2=h2, redir=redir)
        self.reply(html.encode("utf-8"))
        return True

    def handle_login(self) -> bool:
        assert self.parser  # !rm
        if self.args.usernames and not (
            self.args.shr and self.vpath.startswith(self.args.shr1)
        ):
            try:
                un = self.parser.require("uname", 64)
            except:
                un = ""
        else:
            un = ""
        pwd = self.parser.require("cppwd", 64)
        try:
            uhash = self.parser.require("uhash", 256)
        except:
            uhash = ""
        self.parser.drop()

        if not pwd:
            raise Pebkac(422, "password cannot be blank")

        if un:
            pwd = "%s:%s" % (un, pwd)

        dst = self.args.SRS
        if self.vpath:
            dst += quotep(self.vpaths)

        dst += self.ourlq()

        uhash = uhash.lstrip("#")
        if uhash not in ("", "-"):
            dst += "&" if "?" in dst else "?"
            dst += "_=1#" + html_escape(uhash, True, True)

        _, msg = self.get_pwd_cookie(pwd)
        h2 = 'continue'
        html = self.j2s("msg", h1=msg, h2=h2, redir=dst)
        self.reply(html.encode("utf-8"))
        return True

    def handle_logout(self) -> bool:
        assert self.parser  # !rm
        self.parser.drop()

        self.log("logout " + self.uname)
        if not self.uname.startswith("s_"):
            self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
        self.get_pwd_cookie("x")

        dst = self.args.idp_logout or (self.args.SRS + "?h")
        h2 = 'continue'
        html = self.j2s("msg", h1="ok bye", h2=h2, redir=dst)
        self.reply(html.encode("utf-8"))
        return True

    def get_pwd_cookie(self, pwd: str) -> tuple[bool, str]:
        uname = self.asrv.sesa.get(pwd)
        if not uname:
            hpwd = self.asrv.ah.hash(pwd)
            uname = self.asrv.iacct.get(hpwd)
            if uname:
                pwd = self.asrv.ases.get(uname) or pwd
        if uname and self.conn.hsrv.ipr:
            znm = self.conn.hsrv.ipr.get(uname)
            if znm and not znm.map(self.ip):
                self.log("username [%s] rejected by --ipr" % (self.uname,), 3)
                uname = ""
        if uname:
            msg = "hi " + uname
            dur = int(60 * 60 * self.args.logout)
        else:
            logpwd = pwd
            if self.args.log_badpwd == 0:
                logpwd = ""
            elif self.args.log_badpwd == 2:
                zb = hashlib.sha512(pwd.encode("utf-8", "replace")).digest()
                logpwd = "%" + ub64enc(zb[:12]).decode("ascii")

            if pwd != "x":
                self.log("invalid password: %r" % (logpwd,), 3)
                self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")

            msg = "naw dude"
            pwd = "x"  # nosec
            dur = 0

        if pwd == "x":
            # reset both plaintext and tls
            # (only affects active tls cookies when tls)
            for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
                ck = gencookie(k, pwd, self.args.R, self.args.cookie_lax, False)
                self.out_headerlist.append(("Set-Cookie", ck))
            self.out_headers.pop("Set-Cookie", None)  # drop keepalive
        else:
            k = "cppws" if self.is_https else "cppwd"
            ck = gencookie(
                k,
                pwd,
                self.args.R,
                self.args.cookie_lax,
                self.is_https,
                dur,
                "; HttpOnly",
            )
            self.out_headers["Set-Cookie"] = ck

        return dur > 0, msg

    def set_idp_cookie(self, ases) -> None:
        k = "cppws" if self.is_https else "cppwd"
        ck = gencookie(
            k,
            ases,
            self.args.R,
            self.args.cookie_lax,
            self.is_https,
            self.args.idp_cookie,
            "; HttpOnly",
        )
        self.out_headers["Set-Cookie"] = ck

    def handle_mkdir(self) -> bool:
        assert self.parser  # !rm
        new_dir = self.parser.require("name", 512)
        self.parser.drop()

        return self._mkdir(vjoin(self.vpath, new_dir))

    def _mkdir(self, vpath: str, dav: bool = False) -> bool:
        nullwrite = self.args.nw
        self.gctx = vpath
        vpath = undot(vpath)
        vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
        if "nosub" in vfs.flags:
            raise Pebkac(403, "mkdir is forbidden below this folder")

        rem = sanitize_vpath(rem)
        fn = vfs.canonical(rem)

        if not nullwrite:
            fdir = os.path.dirname(fn)

            if dav and not bos.path.isdir(fdir):
                raise Pebkac(409, "parent folder does not exist")

            if bos.path.isdir(fn):
                raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))

            try:
                bos.makedirs(fn, vf=vfs.flags)
            except OSError as ex:
                if ex.errno == errno.EACCES:
                    raise Pebkac(500, "the server OS denied write-access")

                raise Pebkac(500, "mkdir failed:\n" + min_ex())
            except:
                raise Pebkac(500, min_ex())

        self.out_headers["X-New-Dir"] = quotep(self.args.RS + vpath)

        if dav:
            self.reply(b"", 201)
        else:
            self.redirect(vpath, status=201)

        return True

    def handle_new_md(self) -> bool:
        assert self.parser  # !rm
        new_file = self.parser.require("name", 512)
        self.parser.drop()

        nullwrite = self.args.nw
        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
        self._assert_safe_rem(rem)

        if not self.can_delete and (
            "." not in new_file
            or new_file.rsplit(".", 1)[1].lower() not in vfs.flags["rw_edit_set"]
        ):
            t = "you can only create %s files because you don't have the delete-permission"
            raise Pebkac(400, t % (vfs.flags["rw_edit"].replace(",", "/")))

        sanitized = sanitize_fn(new_file)
        fdir = vfs.canonical(rem)
        fn = os.path.join(fdir, sanitized)

        for hn in ("xbu", "xau"):
            xxu = vfs.flags.get(hn)
            if xxu:
                hr = runhook(
                    self.log,
                    self.conn.hsrv.broker,
                    None,
                    "%s.http.new-md" % (hn,),
                    xxu,
                    fn,
                    vjoin(self.vpath, sanitized),
                    self.host,
                    self.uname,
                    self.asrv.vfs.get_perms(self.vpath, self.uname),
                    time.time(),
                    0,
                    self.ip,
                    time.time(),
                    None,
                )
                t = hr.get("rejectmsg") or ""
                if t or hr.get("rc") != 0:
                    if not t:
                        t = "new-md blocked by " + hn + " server config: %r"
                        t = t % (vjoin(vfs.vpath, rem),)
                    self.log(t, 1)
                    raise Pebkac(403, t)

        if not nullwrite:
            if bos.path.exists(fn):
                raise Pebkac(500, "that file exists already")

            with open(fsenc(fn), "wb") as f:
                if "fperms" in vfs.flags:
                    set_fperms(f, vfs.flags)

            dbv, vrem = vfs.get_dbv(rem)
            self.conn.hsrv.broker.say(
                "up2k.hash_file",
                dbv.realpath,
                dbv.vpath,
                dbv.flags,
                vrem,
                sanitized,
                self.ip,
                bos.stat(fn).st_mtime,
                self.uname,
                True,
            )

        vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
        self.redirect(vpath, "?edit")
        return True

    def upload_flags(self, vfs: VFS) -> tuple[int, int, list[str], list[str]]:
        if self.args.nw:
            rnd = 0
        else:
            rnd = int(self.uparam.get("rand") or self.headers.get("rand") or 0)
            if vfs.flags.get("rand"):  # force-enable
                rnd = max(rnd, vfs.flags["nrand"])

        zs = self.uparam.get("life", self.headers.get("life", ""))
        if zs:
            vlife = vfs.flags.get("lifetime") or 0
            lifetime = max(0, int(vlife - int(zs)))
        else:
            lifetime = 0

        return (
            rnd,
            lifetime,
            vfs.flags.get("xbu") or [],
            vfs.flags.get("xau") or [],
        )

    def handle_plain_upload(
        self,
        file0: list[tuple[str, Optional[str], Generator[bytes, None, None]]],
        nohash: bool,
    ) -> bool:
        assert self.parser
        nullwrite = self.args.nw
        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
        self._assert_safe_rem(rem)

        hasher = None
        if nohash:
            halg = ""
            copier = justcopy
        else:
            copier = hashcopy
            halg = (
                self.ouparam.get("ck") or self.headers.get("ck") or vfs.flags["bup_ck"]
            )
            if halg == "sha512":
                pass
            elif halg == "no":
                copier = justcopy
                halg = ""
            elif halg == "md5":
                hasher = hashlib.md5(**USED4SEC)
            elif halg == "sha1":
                hasher = hashlib.sha1(**USED4SEC)
            elif halg == "sha256":
                hasher = hashlib.sha256(**USED4SEC)
            elif halg in ("blake2", "b2"):
                hasher = hashlib.blake2b(**USED4SEC)
            elif halg in ("blake2s", "b2s"):
                hasher = hashlib.blake2s(**USED4SEC)
            else:
                raise Pebkac(500, "unknown hash alg")

        upload_vpath = self.vpath
        lim = vfs.get_dbv(rem)[0].lim
        fdir_base = vfs.canonical(rem)
        if lim:
            fdir_base, rem = lim.all(
                self.ip, rem, -1, vfs.realpath, fdir_base, self.conn.hsrv.broker
            )
            upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
            if not nullwrite:
                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 ""
        if zs:
            zs = zs.split(";", 1)[0].lower()
            if zs == "application/json":
                zs = "json"
        want_url = zs == "url"
        want_json = zs == "json" or "j" in self.uparam

        files: list[tuple[int, str, str, str, str, str]] = []
        # sz, sha_hex, sha_b64, p_file, fname, abspath
        errmsg = ""
        tabspath = ""
        dip = self.dip()
        t0 = time.time()
        try:
            assert self.parser.gen
            gens = itertools.chain(file0, self.parser.gen)
            for nfile, (p_field, p_file, p_data) in enumerate(gens):
                if not p_file:
                    self.log("discarding incoming file without filename")
                    # fallthrough

                fdir = fdir_base
                fname = sanitize_fn(p_file or "")
                abspath = os.path.join(fdir, fname)
                suffix = "-%.6f-%s" % (time.time(), dip)
                if p_file and not nullwrite:
                    if rnd:
                        fname = rand_name(fdir, fname, rnd)

                    open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}

                    if "replace" in self.uparam or "replace" in self.headers:
                        if not self.can_delete:
                            self.log("user not allowed to overwrite with ?replace")
                        elif bos.path.exists(abspath):
                            try:
                                wunlink(self.log, abspath, vfs.flags)
                                t = "overwriting file with new upload: %r"
                            except:
                                t = "toctou while deleting for ?replace: %r"
                            self.log(t % (abspath,))
                else:
                    open_args = {}
                    tnam = fname = os.devnull
                    fdir = abspath = ""

                if xbu:
                    at = time.time() - lifetime
                    hr = runhook(
                        self.log,
                        self.conn.hsrv.broker,
                        None,
                        "xbu.http.bup",
                        xbu,
                        abspath,
                        vjoin(upload_vpath, fname),
                        self.host,
                        self.uname,
                        self.asrv.vfs.get_perms(upload_vpath, self.uname),
                        at,
                        0,
                        self.ip,
                        at,
                        None,
                    )
                    t = hr.get("rejectmsg") or ""
                    if t or hr.get("rc") != 0:
                        if not t:
                            t = "upload blocked by xbu server config: %r"
                            t = t % (vjoin(upload_vpath, fname),)
                        self.log(t, 1)
                        raise Pebkac(403, t)
                    if hr.get("reloc"):
                        zs = vjoin(upload_vpath, fname)
                        x = pathmod(self.asrv.vfs, abspath, zs, hr["reloc"])
                        if x:
                            if self.args.hook_v:
                                log_reloc(
                                    self.log,
                                    hr["reloc"],
                                    x,
                                    abspath,
                                    zs,
                                    fname,
                                    vfs,
                                    rem,
                                )
                            fdir, upload_vpath, fname, (vfs, rem) = x
                            abspath = os.path.join(fdir, fname)
                            if nullwrite:
                                fdir = abspath = ""
                            else:
                                open_args["fdir"] = fdir

                if p_file and not nullwrite:
                    bos.makedirs(fdir, vf=vfs.flags)

                    # reserve destination filename
                    f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
                    f.close()

                    tnam = fname + ".PARTIAL"
                    if self.args.dotpart:
                        tnam = "." + tnam

                    abspath = os.path.join(fdir, fname)
                else:
                    open_args = {}
                    tnam = fname = os.devnull
                    fdir = abspath = ""

                if lim:
                    lim.chk_bup(self.ip)
                    lim.chk_nup(self.ip)

                try:
                    max_sz = 0
                    if lim:
                        v1 = lim.smax
                        v2 = lim.dfv - lim.dfl
                        max_sz = min(v1, v2) if v1 and v2 else v1 or v2

                    f, tnam = ren_open(tnam, "wb", self.args.iobuf, **open_args)
                    try:
                        tabspath = os.path.join(fdir, tnam)
                        self.log("writing to %r" % (tabspath,))
                        sz, sha_hex, sha_b64 = copier(
                            p_data, f, hasher, max_sz, self.args.s_wr_slp
                        )
                    finally:
                        f.close()

                    if lim:
                        lim.nup(self.ip)
                        lim.bup(self.ip, sz)
                        try:
                            lim.chk_df(tabspath, sz, True)
                            lim.chk_sz(sz)
                            lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)
                            lim.chk_bup(self.ip)
                            lim.chk_nup(self.ip)
                        except:
                            if not nullwrite:
                                wunlink(self.log, tabspath, vfs.flags)
                                wunlink(self.log, abspath, vfs.flags)
                            fname = os.devnull
                            raise

                    if not nullwrite:
                        atomic_move(self.log, tabspath, abspath, vfs.flags)

                    tabspath = ""

                    at = time.time() - lifetime
                    if xau:
                        hr = runhook(
                            self.log,
                            self.conn.hsrv.broker,
                            None,
                            "xau.http.bup",
                            xau,
                            abspath,
                            vjoin(upload_vpath, fname),
                            self.host,
                            self.uname,
                            self.asrv.vfs.get_perms(upload_vpath, self.uname),
                            at,
                            sz,
                            self.ip,
                            at,
                            None,
                        )
                        t = hr.get("rejectmsg") or ""
                        if t or hr.get("rc") != 0:
                            if not t:
                                t = "upload blocked by xau server config: %r"
                                t = t % (vjoin(upload_vpath, fname),)
                            self.log(t, 1)
                            wunlink(self.log, abspath, vfs.flags)
                            raise Pebkac(403, t)
                        if hr.get("reloc"):
                            zs = vjoin(upload_vpath, fname)
                            x = pathmod(self.asrv.vfs, abspath, zs, hr["reloc"])
                            if x:
                                if self.args.hook_v:
                                    log_reloc(
                                        self.log,
                                        hr["reloc"],
                                        x,
                                        abspath,
                                        zs,
                                        fname,
                                        vfs,
                                        rem,
                                    )
                                fdir, upload_vpath, fname, (vfs, rem) = x
                                ap2 = os.path.join(fdir, fname)
                                if nullwrite:
                                    fdir = ap2 = ""
                                else:
                                    bos.makedirs(fdir, vf=vfs.flags)
                                    atomic_move(self.log, abspath, ap2, vfs.flags)
                                abspath = ap2
                        sz = bos.path.getsize(abspath)

                    files.append(
                        (sz, sha_hex, sha_b64, p_file or "(discarded)", fname, abspath)
                    )
                    dbv, vrem = vfs.get_dbv(rem)
                    self.conn.hsrv.broker.say(
                        "up2k.hash_file",
                        dbv.realpath,
                        vfs.vpath,
                        dbv.flags,
                        vrem,
                        fname,
                        self.ip,
                        at,
                        self.uname,
                        True,
                    )
                    self.conn.nbyte += sz

                except Pebkac:
                    self.parser.drop()
                    raise

        except Pebkac as ex:
            errmsg = vol_san(
                list(self.asrv.vfs.all_vols.values()), unicode(ex).encode("utf-8")
            ).decode("utf-8")
            try:
                got = bos.path.getsize(tabspath)
                t = "connection lost after receiving %s of the file"
                self.log(t % (humansize(got),), 3)
            except:
                pass

        td = max(0.1, time.time() - t0)
        sz_total = sum(x[0] for x in files)
        spd = (sz_total / td) / (1024 * 1024)

        status = "OK"
        if errmsg:
            self.log(errmsg, 3)
            status = "ERROR"

        msg = "{} // {} bytes // {:.3f} MiB/s\n".format(status, sz_total, spd)
        jmsg: dict[str, Any] = {
            "status": status,
            "sz": sz_total,
            "mbps": round(spd, 3),
            "files": [],
        }

        if errmsg:
            msg += errmsg + "\n"
            jmsg["error"] = errmsg
            errmsg = "ERROR: " + errmsg

        if halg:
            file_fmt = '{0}: {1} // {2} // {3} bytes // {5} {6}\n'
        else:
            file_fmt = '{3} bytes // {5} {6}\n'

        for sz, sha_hex, sha_b64, ofn, lfn, ap in files:
            vsuf = ""
            if (self.can_read or self.can_upget) and "fk" in vfs.flags:
                st = A_FILE if nullwrite else bos.stat(ap)
                alg = 2 if "fka" in vfs.flags else 1
                vsuf = "?k=" + self.gen_fk(
                    alg,
                    self.args.fk_salt,
                    ap,
                    st.st_size,
                    0 if ANYWIN or not ap else st.st_ino,
                )[: vfs.flags["fk"]]

            if "media" in self.uparam or "medialinks" in vfs.flags:
                vsuf += "&v" if vsuf else "?v"

            vpath = vjoin(upload_vpath, lfn)
            if self.args.up_site:
                ah_url = j_url = self.args.up_site + self.args.RS + quotep(vpath) + vsuf
                rel_url = "/" + j_url.split("//", 1)[-1].split("/", 1)[-1]
            else:
                ah_url = rel_url = "/%s%s%s" % (self.args.RS, quotep(vpath), vsuf)
                j_url = "%s://%s%s" % (
                    "https" if self.is_https else "http",
                    self.host,
                    rel_url,
                )

            msg += file_fmt.format(
                halg,
                sha_hex[:56],
                sha_b64,
                sz,
                ah_url,
                html_escape(ofn, crlf=True),
                vsuf,
            )
            # truncated SHA-512 prevents length extension attacks;
            # using SHA-512/224, optionally SHA-512/256 = :64
            jpart = {
                "url": j_url,
                "sz": sz,
                "fn": lfn,
                "fn_orig": ofn,
                "path": rel_url,
            }
            if halg:
                jpart[halg] = sha_hex[:56]
                jpart["sha_b64"] = sha_b64
            jmsg["files"].append(jpart)

        vspd = self._spd(sz_total, False)
        self.log("%s %r" % (vspd, msg))

        suf = ""
        if not nullwrite and self.args.write_uplog:
            try:
                log_fn = "up.{:.6f}.txt".format(t0)
                with open(log_fn, "wb") as f:
                    ft = "{}:{}".format(self.ip, self.addr[1])
                    ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
                    f.write(ft.encode("utf-8"))
                    if "fperms" in vfs.flags:
                        set_fperms(f, vfs.flags)
            except Exception as ex:
                suf = "\nfailed to write the upload report: {}".format(ex)

        sc = 400 if errmsg else 201
        if want_url:
            msg = "\n".join([x["url"] for x in jmsg["files"]])
            if errmsg:
                msg += "\n" + errmsg

            self.reply(msg.encode("utf-8", "replace"), status=sc)
        elif want_json:
            if len(jmsg["files"]) == 1:
                jmsg["fileurl"] = jmsg["files"][0]["url"]
            jtxt = json.dumps(jmsg, indent=2, sort_keys=True).encode("utf-8", "replace")
            self.reply(jtxt, mime="application/json", status=sc)
        else:
            self.redirect(
                self.vpath,
                msg=msg + suf,
                flavor="return to",
                click=False,
                status=sc,
            )

        if errmsg:
            return False

        self.parser.drop()
        return True

    def handle_text_upload(self) -> bool:
        assert self.parser  # !rm
        try:
            cli_lastmod3 = int(self.parser.require("lastmod", 16))
        except:
            raise Pebkac(400, "could not read lastmod from request")

        nullwrite = self.args.nw
        vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, True, True)
        self._assert_safe_rem(rem)

        clen = int(self.headers.get("content-length", -1))
        if clen == -1:
            raise Pebkac(411)

        rp, fn = vsplit(rem)
        fp = vfs.canonical(rp)
        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, vf=vfs.flags)

        fp = os.path.join(fp, fn)
        rem = "{}/{}".format(rp, fn).strip("/")
        dbv, vrem = vfs.get_dbv(rem)

        if not self.can_delete and (
            "." not in rem
            or rem.rsplit(".", 1)[1].lower() not in vfs.flags["rw_edit_set"]
        ):
            t = "you can only edit %s files because you don't have the delete-permission"
            raise Pebkac(400, t % (vfs.flags["rw_edit"].replace(",", "/")))

        if nullwrite:
            response = json.dumps({"ok": True, "lastmod": 0})
            self.log(response)
            # TODO reply should parser.drop()
            self.parser.drop()
            self.reply(response.encode("utf-8"))
            return True

        srv_lastmod = -1.0
        srv_lastmod3 = -1
        try:
            st = bos.stat(fp)
            srv_lastmod = st.st_mtime
            srv_lastmod3 = int(srv_lastmod * 1000)
        except OSError as ex:
            if ex.errno != errno.ENOENT:
                raise

        # if file exists, check that timestamp matches the client's
        if srv_lastmod >= 0:
            same_lastmod = cli_lastmod3 in [-1, srv_lastmod3]
            if not same_lastmod:
                # some filesystems/transports limit precision to 1sec, hopefully floored
                same_lastmod = (
                    srv_lastmod == int(cli_lastmod3 / 1000)
                    and cli_lastmod3 > srv_lastmod3
                    and cli_lastmod3 - srv_lastmod3 < 1000
                )

            if not same_lastmod:
                response = json.dumps(
                    {
                        "ok": False,
                        "lastmod": srv_lastmod3,
                        "now": int(time.time() * 1000),
                    }
                )
                self.log(
                    "{} - {} = {}".format(
                        srv_lastmod3, cli_lastmod3, srv_lastmod3 - cli_lastmod3
                    )
                )
                self.log(response)
                self.parser.drop()
                self.reply(response.encode("utf-8"))
                return True

            mdir, mfile = os.path.split(fp)
            fname, fext = mfile.rsplit(".", 1) if "." in mfile else (mfile, "md")
            mfile2 = "{}.{:.3f}.{}".format(fname, srv_lastmod, fext)

            dp = ""
            hist_cfg = dbv.flags["md_hist"]
            if hist_cfg == "v":
                vrd = vsplit(vrem)[0]
                zb = hashlib.sha512(afsenc(vrd)).digest()
                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, vf=vfs.flags):
                    with open(os.path.join(dp, "dir.txt"), "wb") as f:
                        f.write(afsenc(vrd))
                        if "fperms" in vfs.flags:
                            set_fperms(f, vfs.flags)
            elif hist_cfg == "s":
                dp = os.path.join(mdir, ".hist")
                try:
                    bos.mkdir(dp, vfs.flags["chmod_d"])
                    if "chown" in vfs.flags:
                        bos.chown(dp, vfs.flags["uid"], vfs.flags["gid"])
                    hidedir(dp)
                except:
                    pass
            if dp:
                atomic_move(self.log, fp, os.path.join(dp, mfile2), vfs.flags)

        assert self.parser.gen  # !rm
        p_field, _, p_data = next(self.parser.gen)
        if p_field != "body":
            raise Pebkac(400, "expected body, got %r" % (p_field,))

        if "txt_eol" in vfs.flags:
            p_data = eol_conv(p_data, vfs.flags["txt_eol"])

        xbu = vfs.flags.get("xbu")
        if xbu:
            hr = runhook(
                self.log,
                self.conn.hsrv.broker,
                None,
                "xbu.http.txt",
                xbu,
                fp,
                self.vpath,
                self.host,
                self.uname,
                self.asrv.vfs.get_perms(self.vpath, self.uname),
                time.time(),
                0,
                self.ip,
                time.time(),
                None,
            )
            t = hr.get("rejectmsg") or ""
            if t or hr.get("rc") != 0:
                if not t:
                    t = "save blocked by xbu server config"
                self.log(t, 1)
                raise Pebkac(403, t)

        if bos.path.exists(fp):
            wunlink(self.log, fp, vfs.flags)

        with open(fsenc(fp), "wb", self.args.iobuf) as f:
            if "fperms" in vfs.flags:
                set_fperms(f, vfs.flags)
            sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)

        if lim:
            lim.nup(self.ip)
            lim.bup(self.ip, sz)
            try:
                lim.chk_sz(sz)
                lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)
            except:
                wunlink(self.log, fp, vfs.flags)
                raise

        new_lastmod = bos.stat(fp).st_mtime
        new_lastmod3 = int(new_lastmod * 1000)
        sha512 = sha512[:56]

        xau = vfs.flags.get("xau")
        if xau:
            hr = runhook(
                self.log,
                self.conn.hsrv.broker,
                None,
                "xau.http.txt",
                xau,
                fp,
                self.vpath,
                self.host,
                self.uname,
                self.asrv.vfs.get_perms(self.vpath, self.uname),
                new_lastmod,
                sz,
                self.ip,
                new_lastmod,
                None,
            )
            t = hr.get("rejectmsg") or ""
            if t or hr.get("rc") != 0:
                if not t:
                    t = "save blocked by xau server config"
                self.log(t, 1)
                wunlink(self.log, fp, vfs.flags)
                raise Pebkac(403, t)

        self.conn.hsrv.broker.say(
            "up2k.hash_file",
            dbv.realpath,
            dbv.vpath,
            dbv.flags,
            vsplit(vrem)[0],
            fn,
            self.ip,
            new_lastmod,
            self.uname,
            True,
        )

        response = json.dumps(
            {"ok": True, "lastmod": new_lastmod3, "size": sz, "sha512": sha512}
        )
        self.log(response)
        self.parser.drop()
        self.reply(response.encode("utf-8"))
        return True

    def _chk_lastmod(self, file_ts: int) -> tuple[str, bool, bool]:
        # ret: lastmod, do_send, can_range
        file_lastmod = formatdate(file_ts)
        c_ifrange = self.headers.get("if-range")
        c_lastmod = self.headers.get("if-modified-since")

        if not c_ifrange and not c_lastmod:
            return file_lastmod, True, True

        if c_ifrange and c_ifrange != file_lastmod:
            t = "sending entire file due to If-Range; cli(%s) file(%s)"
            self.log(t % (c_ifrange, file_lastmod), 6)
            return file_lastmod, True, False

        do_send = c_lastmod != file_lastmod
        if do_send and c_lastmod:
            t = "sending body due to If-Modified-Since cli(%s) file(%s)"
            self.log(t % (c_lastmod, file_lastmod), 6)
        elif not do_send and self.no304():
            do_send = True
            self.log("sending body due to no304")

        return file_lastmod, do_send, True

    def _use_dirkey(self, vn: VFS, ap: str) -> bool:
        if self.can_read or not self.can_get:
            return False

        if vn.flags.get("dky"):
            return True

        req = self.uparam.get("k") or ""
        if not req:
            return False

        dk_len = vn.flags.get("dk")
        if not dk_len:
            return False

        if not ap:
            ap = vn.canonical(self.rem)

        zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_len]
        if req == zs:
            return True

        t = "wrong dirkey, want %s, got %s\n  vp: %r\n  ap: %r"
        self.log(t % (zs, req, self.req, ap), 6)
        return False

    def _use_filekey(self, vn: VFS, ap: str, st: os.stat_result) -> bool:
        if self.can_read or not self.can_get:
            return False

        req = self.uparam.get("k") or ""
        if not req:
            return False

        fk_len = vn.flags.get("fk")
        if not fk_len:
            return False

        if not ap:
            ap = self.vn.canonical(self.rem)

        alg = 2 if "fka" in vn.flags else 1

        zs = self.gen_fk(
            alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
        )[:fk_len]

        if req == zs:
            return True

        t = "wrong filekey, want %s, got %s\n  vp: %r\n  ap: %r"
        self.log(t % (zs, req, self.req, ap), 6)
        return False

    def _add_logues(
        self, vn: VFS, abspath: str, lnames: Optional[dict[str, str]]
    ) -> tuple[list[str], list[str]]:
        logues = ["", ""]
        for n, fns1, fns2 in [] if self.args.no_logues else vn.flags["emb_lgs"]:
            for fn in fns1 if lnames is None else fns2:
                if lnames is not None:
                    fn = lnames.get(fn)
                    if not fn:
                        continue
                fn = "%s/%s" % (abspath, fn)
                if not bos.path.isfile(fn):
                    continue
                logues[n] = read_utf8(self.log, fsenc(fn), False)
                if "exp" in vn.flags:
                    logues[n] = self._expand(logues[n], vn.flags.get("exp_lg") or [])
                break

        readmes = ["", ""]
        for n, fns1, fns2 in [] if self.args.no_readme else vn.flags["emb_mds"]:
            if logues[n]:
                continue
            for fn in fns1 if lnames is None else fns2:
                if lnames is not None:
                    fn = lnames.get(fn.lower())
                    if not fn:
                        continue
                fn = "%s/%s" % (abspath, fn)
                if not bos.path.isfile(fn):
                    continue
                readmes[n] = read_utf8(self.log, fsenc(fn), False)
                if "exp" in vn.flags:
                    readmes[n] = self._expand(readmes[n], vn.flags.get("exp_md") or [])
                break

        return logues, readmes

    def _expand(self, txt: str, phs: list[str]) -> str:
        ptn_hsafe = RE_HSAFE
        for ph in phs:
            if ph.startswith("hdr."):
                sv = str(self.headers.get(ph[4:], ""))
            elif ph.startswith("self."):
                sv = str(getattr(self, ph[5:], ""))
            elif ph.startswith("cfg."):
                sv = str(getattr(self.args, ph[4:], ""))
            elif ph.startswith("vf."):
                sv = str(self.vn.flags.get(ph[3:]) or "")
            elif ph == "srv.itime":
                sv = str(int(time.time()))
            elif ph == "srv.htime":
                sv = datetime.now(UTC).strftime("%Y-%m-%d, %H:%M:%S")
            else:
                self.log("unknown placeholder in server config: [%s]" % (ph,), 3)
                continue

            sv = ptn_hsafe.sub("_", sv)
            txt = txt.replace("{{%s}}" % (ph,), sv)

        return txt

    def _can_tail(self, volflags: dict[str, Any]) -> bool:
        zp = self.args.ua_nodoc
        if zp and zp.search(self.ua):
            t = "this URL contains no valuable information for bots/crawlers"
            raise Pebkac(403, t)
        lvl = volflags["tail_who"]
        if "notail" in volflags or not lvl:
            raise Pebkac(400, "tail is disabled in server config")
        elif lvl <= 1 and not self.can_admin:
            raise Pebkac(400, "tail is admin-only on this server")
        elif lvl <= 2 and self.uname in ("", "*"):
            raise Pebkac(400, "you must be authenticated to use ?tail on this server")
        return True

    def _can_zip(self, volflags: dict[str, Any]) -> str:
        lvl = volflags["zip_who"]
        if self.args.no_zip or not lvl:
            return "download-as-zip/tar is disabled in server config"
        elif lvl <= 1 and not self.can_admin:
            return "download-as-zip/tar is admin-only on this server"
        elif lvl <= 2 and self.uname in ("", "*"):
            return "you must be authenticated to download-as-zip/tar on this server"
        elif self.args.ua_nozip and self.args.ua_nozip.search(self.ua):
            t = "this URL contains no valuable information for bots/crawlers"
            raise Pebkac(403, t)
        return ""

    def tx_res(self, req_path: str) -> bool:
        status = 200
        logmsg = "{:4} {} ".format("", self.req)
        logtail = ""

        editions = {}
        file_ts = 0

        if has_resource(self.E, req_path):
            st = stat_resource(self.E, req_path)
            if st:
                file_ts = max(file_ts, st.st_mtime)
            editions["plain"] = req_path

        if has_resource(self.E, req_path + ".gz"):
            st = stat_resource(self.E, req_path + ".gz")
            if st:
                file_ts = max(file_ts, st.st_mtime)
            if not st or st.st_mtime > file_ts:
                editions[".gz"] = req_path + ".gz"

        if not editions:
            return self.tx_404()

        #
        # force download

        if "dl" in self.ouparam:
            cdis = self.ouparam["dl"] or req_path
            zs = gen_content_disposition(os.path.basename(cdis))
            self.out_headers["Content-Disposition"] = zs
        else:
            cdis = req_path

        #
        # if-modified

        if file_ts > 0:
            file_lastmod, do_send, _ = self._chk_lastmod(int(file_ts))
            self.out_headers["Last-Modified"] = file_lastmod
            if not do_send:
                status = 304

            if self.can_write:
                self.out_headers["X-Lastmod3"] = str(int(file_ts * 1000))
        else:
            do_send = True

        #
        # Accept-Encoding and UA decides which edition to send

        decompress = False
        supported_editions = [
            x.strip()
            for x in self.headers.get("accept-encoding", "").lower().split(",")
        ]
        if ".gz" in editions:
            is_compressed = True
            selected_edition = ".gz"

            if "gzip" not in supported_editions:
                decompress = True
            else:
                if re.match(r"MSIE [4-6]\.", self.ua) and " SV1" not in self.ua:
                    decompress = True

            if not decompress:
                self.out_headers["Content-Encoding"] = "gzip"
        else:
            is_compressed = False
            selected_edition = "plain"

        res_path = editions[selected_edition]
        logmsg += "{} ".format(selected_edition.lstrip("."))

        res = load_resource(self.E, res_path)

        if decompress:
            file_sz = gzip_file_orig_sz(res)
            res = gzip.open(res)
        else:
            res.seek(0, os.SEEK_END)
            file_sz = res.tell()
            res.seek(0, os.SEEK_SET)

        #
        # send reply

        if is_compressed:
            self.out_headers["Cache-Control"] = "max-age=604869"
        else:
            self.permit_caching()

        if "txt" in self.uparam:
            mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
        elif "mime" in self.uparam:
            mime = str(self.uparam.get("mime"))
        else:
            mime = guess_mime(cdis)

        logmsg += unicode(status) + logtail

        if self.mode == "HEAD" or not do_send:
            res.close()
            if self.do_log:
                self.log(logmsg)

            self.send_headers("oh_g", length=file_sz, status=status, mime=mime)
            return True

        ret = True
        self.send_headers("oh_g", length=file_sz, status=status, mime=mime)
        remains = sendfile_py(
            self.log,
            0,
            file_sz,
            res,
            self.s,
            self.args.s_wr_sz,
            self.args.s_wr_slp,
            not self.args.no_poll,
            {},
            "",
        )
        res.close()

        if remains > 0:
            logmsg += " \033[31m" + unicode(file_sz - remains) + "\033[0m"
            ret = False

        spd = self._spd(file_sz - remains)
        if self.do_log:
            self.log("{},  {}".format(logmsg, spd))

        return ret

    def tx_file(self, oh_k: str, req_path: str, ptop: Optional[str] = None) -> bool:
        status = 200
        logmsg = "{:4} {} ".format("", self.req)
        logtail = ""

        is_tail = "tail" in self.uparam and self._can_tail(self.vn.flags)

        if ptop is not None:
            ap_data = "<%s>" % (req_path,)
            try:
                dp, fn = os.path.split(req_path)
                tnam = fn + ".PARTIAL"
                if self.args.dotpart:
                    tnam = "." + tnam
                ap_data = os.path.join(dp, tnam)
                st_data = bos.stat(ap_data)
                if not st_data.st_size:
                    raise Exception("partial is empty")
                x = self.conn.hsrv.broker.ask("up2k.find_job_by_ap", ptop, req_path)
                job = json.loads(x.get())
                if not job:
                    raise Exception("not found in registry")
                self.pipes.set(req_path, job)
            except Exception as ex:
                if getattr(ex, "errno", 0) != errno.ENOENT:
                    self.log("will not pipe %r; %s" % (ap_data, ex), 6)
                ptop = None

        #
        # if request is for foo.js, check if we have foo.js.gz

        file_ts = 0.0
        editions: dict[str, tuple[str, int]] = {}
        for ext in ("", ".gz"):
            if ptop is not None:
                assert job and ap_data  # type: ignore  # !rm
                sz = job["size"]
                file_ts = max(0, job["lmod"])
                editions["plain"] = (ap_data, sz)
                break

            try:
                fs_path = req_path + ext
                st = bos.stat(fs_path)
                if stat.S_ISDIR(st.st_mode):
                    continue

                sz = st.st_size
                if stat.S_ISBLK(st.st_mode):
                    fd = bos.open(fs_path, os.O_RDONLY)
                    try:
                        sz = os.lseek(fd, 0, os.SEEK_END)
                    finally:
                        os.close(fd)

                file_ts = max(file_ts, st.st_mtime)
                editions[ext or "plain"] = (fs_path, sz)
            except:
                pass
            if not self.vpath.startswith(".cpr/"):
                break

        if not editions:
            return self.tx_404()

        #
        # force download

        if "dl" in self.ouparam:
            cdis = self.ouparam["dl"] or req_path
            zs = gen_content_disposition(os.path.basename(cdis))
            self.out_headers["Content-Disposition"] = zs
        else:
            cdis = req_path

        #
        # if-modified

        file_lastmod, do_send, can_range = self._chk_lastmod(int(file_ts))
        self.out_headers["Last-Modified"] = file_lastmod
        if not do_send:
            status = 304

        if self.can_write:
            self.out_headers["X-Lastmod3"] = str(int(file_ts * 1000))

        #
        # Accept-Encoding and UA decides which edition to send

        decompress = False
        supported_editions = [
            x.strip()
            for x in self.headers.get("accept-encoding", "").lower().split(",")
        ]
        if ".gz" in editions:
            is_compressed = True
            selected_edition = ".gz"
            fs_path, file_sz = editions[".gz"]
            if "gzip" not in supported_editions:
                decompress = True
            else:
                if re.match(r"MSIE [4-6]\.", self.ua) and " SV1" not in self.ua:
                    decompress = True

            if not decompress:
                self.out_headers["Content-Encoding"] = "gzip"
        else:
            is_compressed = False
            selected_edition = "plain"

        fs_path, file_sz = editions[selected_edition]
        logmsg += "{} ".format(selected_edition.lstrip("."))

        #
        # partial

        lower = 0
        upper = file_sz
        hrange = self.headers.get("range")

        # let's not support 206 with compression
        # and multirange / multipart is also not-impl (mostly because calculating contentlength is a pain)
        if (
            do_send
            and not is_compressed
            and hrange
            and can_range
            and file_sz
            and "," not in hrange
            and not is_tail
        ):
            try:
                if not hrange.lower().startswith("bytes"):
                    raise Exception()

                a, b = hrange.split("=", 1)[1].split("-")

                if a.strip():
                    lower = int(a.strip())
                else:
                    lower = 0

                if b.strip():
                    upper = int(b.strip()) + 1
                else:
                    upper = file_sz

                if upper > file_sz:
                    upper = file_sz

                if lower < 0 or lower >= upper:
                    raise Exception()

            except:
                err = "invalid range ({}), size={}".format(hrange, file_sz)
                self.loud_reply(
                    err,
                    status=416,
                    headers={"Content-Range": "bytes */{}".format(file_sz)},
                )
                return True

            status = 206
            self.out_headers["Content-Range"] = "bytes {}-{}/{}".format(
                lower, upper - 1, file_sz
            )

            logtail += " [\033[36m{}-{}\033[0m]".format(lower, upper)

        use_sendfile = False
        if decompress:
            open_func: Any = gzip.open
            open_args: list[Any] = [fsenc(fs_path), "rb"]
            # Content-Length := original file size
            upper = gzip_orig_sz(fs_path)
        else:
            open_func = open
            open_args = [fsenc(fs_path), "rb", self.args.iobuf]
            use_sendfile = (
                # fmt: off
                not self.tls
                and not self.args.no_sendfile
                and (BITNESS > 32 or file_sz < 0x7fffFFFF)
                # fmt: on
            )

        #
        # send reply

        if is_compressed:
            self.out_headers["Cache-Control"] = "max-age=604869"
        else:
            self.permit_caching()

        if "txt" in self.uparam:
            mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
        elif "mime" in self.uparam:
            mime = str(self.uparam.get("mime"))
        elif "rmagic" in self.vn.flags:
            mime = guess_mime(req_path, fs_path)
        else:
            mime = guess_mime(cdis)

        if mime not in SAFE_MIMES and "nohtml" in self.vn.flags and oh_k != "oh_g":
            mime = safe_mime(mime)

        self.out_headers["Accept-Ranges"] = "bytes"
        logmsg += unicode(status) + logtail

        if self.mode == "HEAD" or not do_send:
            if self.do_log:
                self.log(logmsg)

            self.send_headers(oh_k, length=upper - lower, status=status, mime=mime)
            return True

        dls = self.conn.hsrv.dls
        if is_tail:
            upper = 1 << 30
            if len(dls) > self.args.tail_cmax:
                raise Pebkac(400, "too many active downloads to start a new tail")

        if upper - lower > 0x400000:  # 4m
            now = time.time()
            self.dl_id = "%s:%s" % (self.ip, self.addr[1])
            dls[self.dl_id] = (now, 0)
            self.conn.hsrv.dli[self.dl_id] = (
                now,
                0 if is_tail else upper - lower,
                self.vn,
                self.vpath,
                self.uname,
            )

        if ptop is not None:
            assert job and ap_data  # type: ignore  # !rm
            return self.tx_pipe(
                ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
            )
        elif is_tail:
            self.tx_tail(open_args, status, mime)
            return False

        ret = True
        with open_func(*open_args) as f:
            self.send_headers(oh_k, length=upper - lower, status=status, mime=mime)

            sendfun = sendfile_kern if use_sendfile else sendfile_py
            remains = sendfun(
                self.log,
                lower,
                upper,
                f,
                self.s,
                self.args.s_wr_sz,
                self.args.s_wr_slp,
                not self.args.no_poll,
                dls,
                self.dl_id,
            )

        if remains > 0:
            logmsg += " \033[31m" + unicode(upper - remains) + "\033[0m"
            ret = False

        spd = self._spd((upper - lower) - remains)
        if self.do_log:
            self.log("{},  {}".format(logmsg, spd))

        return ret

    def tx_tail(
        self,
        open_args: list[Any],
        status: int,
        mime: str,
    ) -> None:
        vf = self.vn.flags
        self.send_headers("oh_f", length=None, status=status, mime=mime)
        abspath: bytes = open_args[0]
        sec_rate = vf["tail_rate"]
        sec_max = vf["tail_tmax"]
        sec_fd = vf["tail_fd"]
        sec_ka = self.args.tail_ka
        wr_slp = self.args.s_wr_slp
        wr_sz = self.args.s_wr_sz
        dls = self.conn.hsrv.dls
        dl_id = self.dl_id

        # non-numeric = full file from start
        # positive = absolute offset from start
        # negative = start that many bytes from eof
        try:
            ofs = int(self.uparam["tail"])
        except:
            ofs = 0

        t0 = time.time()
        ofs0 = ofs
        f = None
        try:
            st = os.stat(abspath)
            f = open_nolock(*open_args)
            f.seek(0, os.SEEK_END)
            eof = f.tell()
            f.seek(0)
            if ofs < 0:
                ofs = max(0, ofs + eof)

            self.log("tailing from byte %d: %r" % (ofs, abspath), 6)

            # send initial data asap
            remains = sendfile_py(
                self.log,  # d/c
                ofs,
                eof,
                f,
                self.s,
                wr_sz,
                wr_slp,
                False,  # d/c
                dls,
                dl_id,
            )
            sent = (eof - ofs) - remains
            ofs = eof - remains
            f.seek(ofs)

            try:
                st2 = os.stat(open_args[0])
                if st.st_ino == st2.st_ino:
                    st = st2  # for filesize
            except:
                pass

            gone = 0
            unsent = False
            t_fd = t_ka = time.time()
            while True:
                assert f  # !rm
                buf = f.read(4096)
                now = time.time()

                if sec_max and now - t0 >= sec_max:
                    self.log("max duration exceeded; kicking client", 6)
                    zb = b"\n\n*** max duration exceeded; disconnecting ***\n"
                    self.s.sendall(zb)
                    break

                if buf:
                    t_fd = t_ka = now
                    self.s.sendall(buf)
                    sent += len(buf)
                    unsent = False
                    dls[dl_id] = (time.time(), sent)
                    continue

                time.sleep(sec_rate)
                if t_ka < now - sec_ka:
                    t_ka = now
                    self.s.send(b"\x00")
                if t_fd < now - sec_fd:
                    try:
                        st2 = os.stat(open_args[0])
                        szd = st2.st_size - st.st_size
                        if (
                            st2.st_ino != st.st_ino
                            or st2.st_size < sent
                            or szd < 0
                            or unsent
                        ):
                            assert f  # !rm
                            # open new file before closing previous to avoid toctous (open may fail; cannot null f before)
                            f2 = open_nolock(*open_args)
                            f.close()
                            f = f2
                            f.seek(0, os.SEEK_END)
                            eof = f.tell()
                            if eof < sent:
                                ofs = sent = 0  # shrunk; send from start
                                zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n"
                                self.s.sendall(zb)
                                if ofs0 < 0 and eof > -ofs0:
                                    ofs = eof + ofs0
                            else:
                                ofs = sent  # just new fd? resume from same ofs
                            f.seek(ofs)
                            self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
                            unsent = False
                            gone = 0
                        elif szd:
                            unsent = True
                        st = st2
                    except:
                        gone += 1
                        if gone > 3:
                            self.log("file deleted; disconnecting")
                            break
        except IOError as ex:
            if ex.errno not in E_SCK_WR:
                raise
        finally:
            if f:
                f.close()

    def tx_pipe(
        self,
        ptop: str,
        req_path: str,
        ap_data: str,
        job: dict[str, Any],
        lower: int,
        upper: int,
        status: int,
        mime: str,
        logmsg: str,
    ) -> bool:
        M = 1048576
        self.send_headers("oh_f", length=upper - lower, status=status, mime=mime)
        wr_slp = self.args.s_wr_slp
        wr_sz = self.args.s_wr_sz
        file_size = job["size"]
        chunk_size = up2k_chunksize(file_size)
        num_need = -1
        data_end = 0
        remains = upper - lower
        broken = False
        spins = 0
        tier = 0
        tiers = ["uncapped", "reduced speed", "one byte per sec"]

        while lower < upper and not broken:
            with self.u2mutex:
                job = self.pipes.get(req_path)
                if not job:
                    x = self.conn.hsrv.broker.ask("up2k.find_job_by_ap", ptop, req_path)
                    job = json.loads(x.get())
                    if job:
                        self.pipes.set(req_path, job)

            if not job:
                t = "pipe: OK, upload has finished; yeeting remainder"
                self.log(t, 2)
                data_end = file_size
                break

            if num_need != len(job["need"]) and data_end - lower < 8 * M:
                num_need = len(job["need"])
                data_end = 0
                for cid in job["hash"]:
                    if cid in job["need"]:
                        break
                    data_end += chunk_size
                t = "pipe: can stream %.2f MiB; requested range is %.2f to %.2f"
                self.log(t % (data_end / M, lower / M, upper / M), 6)
                with self.u2mutex:
                    if data_end > self.u2fh.aps.get(ap_data, data_end):
                        fhs: Optional[set[typing.BinaryIO]] = None
                        try:
                            fhs = self.u2fh.cache[ap_data].all_fhs
                            for fh in fhs:
                                fh.flush()
                            self.u2fh.aps[ap_data] = data_end
                            self.log("pipe: flushed %d up2k-FDs" % (len(fhs),))
                        except Exception as ex:
                            if fhs is None:
                                err = "file is not being written to right now"
                            else:
                                err = repr(ex)
                            self.log("pipe: u2fh flush failed: " + err)

            if lower >= data_end:
                if data_end:
                    t = "pipe: uploader is too slow; aborting download at %.2f MiB"
                    self.log(t % (data_end / M,))
                    raise Pebkac(416, "uploader is too slow")

                raise Pebkac(416, "no data available yet; please retry in a bit")

            slack = data_end - lower
            if slack >= 8 * M:
                ntier = 0
                winsz = M
                bufsz = wr_sz
                slp = wr_slp
            else:
                winsz = max(40, int(M * (slack / (12 * M))))
                base_rate = M if not wr_slp else wr_sz / wr_slp
                if winsz > base_rate:
                    ntier = 0
                    bufsz = wr_sz
                    slp = wr_slp
                elif winsz > 300:
                    ntier = 1
                    bufsz = winsz // 5
                    slp = 0.2
                else:
                    ntier = 2
                    bufsz = winsz = slp = 1

            if tier != ntier:
                tier = ntier
                self.log("moved to tier %d (%s)" % (tier, tiers[tier]))

            try:
                with open_nolock(ap_data, "rb", self.args.iobuf) as f:
                    f.seek(lower)
                    page = f.read(min(winsz, data_end - lower, upper - lower))
                if not page:
                    raise Exception("got 0 bytes (EOF?)")
            except Exception as ex:
                self.log("pipe: read failed at %.2f MiB: %s" % (lower / M, ex), 3)
                with self.u2mutex:
                    self.pipes.c.pop(req_path, None)
                spins += 1
                if spins > 3:
                    raise Pebkac(500, "file became unreadable")
                time.sleep(2)
                continue

            spins = 0
            pofs = 0
            while pofs < len(page):
                if slp:
                    time.sleep(slp)

                try:
                    buf = page[pofs : pofs + bufsz]
                    self.s.sendall(buf)
                    zi = len(buf)
                    remains -= zi
                    lower += zi
                    pofs += zi
                except:
                    broken = True
                    break

        if lower < upper and not broken:
            with open_nolock(req_path, "rb") as f:
                remains = sendfile_py(
                    self.log,
                    lower,
                    upper,
                    f,
                    self.s,
                    wr_sz,
                    wr_slp,
                    not self.args.no_poll,
                    self.conn.hsrv.dls,
                    self.dl_id,
                )

        spd = self._spd((upper - lower) - remains)
        if self.do_log:
            self.log("{},  {}".format(logmsg, spd))

        return not broken

    def tx_zip(
        self,
        fmt: str,
        uarg: str,
        vpath: str,
        vn: VFS,
        rem: str,
        items: list[str],
    ) -> bool:
        t = self._can_zip(vn.flags)
        if t:
            raise Pebkac(400, t)

        logmsg = "{:4} {} ".format("", self.req)
        self.keepalive = False

        cancmp = not self.args.no_tarcmp

        if fmt == "tar":
            packer: Type[StreamArc] = StreamTar
            if cancmp and "gz" in uarg:
                mime = "application/gzip"
                ext = "tar.gz"
            elif cancmp and "bz2" in uarg:
                mime = "application/x-bzip"
                ext = "tar.bz2"
            elif cancmp and "xz" in uarg:
                mime = "application/x-xz"
                ext = "tar.xz"
            else:
                mime = "application/x-tar"
                ext = "tar"
        else:
            mime = "application/zip"
            packer = StreamZip
            ext = "zip"

        dots = 0 if "nodot" in self.uparam else 1
        scandir = not self.args.no_scandir

        fn = self.vpath.split("/")[-1] or self.host.split(":")[0]
        if items:
            fn = "sel-" + fn

        if vn.flags.get("zipmax") and not (
            vn.flags.get("zipmaxu") and self.uname != "*"
        ):
            maxs = vn.flags.get("zipmaxs_v") or 0
            maxn = vn.flags.get("zipmaxn_v") or 0
            nf = 0
            nb = 0
            fgen = vn.zipgen(vpath, rem, set(items), self.uname, False, dots, scandir)
            t = "total size exceeds a limit specified in server config"
            t = vn.flags.get("zipmaxt") or t
            if maxs and maxn:
                for zd in fgen:
                    nf += 1
                    nb += zd["st"].st_size
                    if maxs < nb or maxn < nf:
                        raise Pebkac(400, t)
            elif maxs:
                for zd in fgen:
                    nb += zd["st"].st_size
                    if maxs < nb:
                        raise Pebkac(400, t)
            elif maxn:
                for zd in fgen:
                    nf += 1
                    if maxn < nf:
                        raise Pebkac(400, t)

        cdis = gen_content_disposition("%s.%s" % (fn, ext))
        self.log(repr(cdis))
        self.send_headers(
            "oh_f", None, mime=mime, headers={"Content-Disposition": cdis}
        )

        fgen = vn.zipgen(vpath, rem, set(items), self.uname, False, dots, scandir)
        # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
        cfmt = ""
        if self.thumbcli and not self.args.no_bacode:
            if uarg in ZIP_XCODE_S:
                cfmt = uarg
            else:
                for zs in ZIP_XCODE_L:
                    if zs in self.ouparam:
                        cfmt = zs

            if cfmt:
                self.log("transcoding to [{}]".format(cfmt))
                fgen = gfilter(fgen, self.thumbcli, self.uname, vpath, cfmt)

        now = time.time()
        self.dl_id = "%s:%s" % (self.ip, self.addr[1])
        self.conn.hsrv.dli[self.dl_id] = (
            now,
            0,
            self.vn,
            "%s :%s" % (self.vpath, ext),
            self.uname,
        )
        dls = self.conn.hsrv.dls
        dls[self.dl_id] = (time.time(), 0)

        bgen = packer(
            self.log,
            self.asrv,
            fgen,
            utf8="utf" in uarg or not uarg,
            pre_crc="crc" in uarg,
            cmp=uarg if cancmp or uarg == "pax" else "",
        )
        n = 0
        bsent = 0
        for buf in bgen.gen():
            if not buf:
                break

            try:
                self.s.sendall(buf)
                bsent += len(buf)
            except:
                logmsg += " \033[31m" + unicode(bsent) + "\033[0m"
                bgen.stop()
                break

            n += 1
            if n >= 4:
                n = 0
                dls[self.dl_id] = (time.time(), bsent)

        spd = self._spd(bsent)
        self.log("{},  {}".format(logmsg, spd))
        return True

    def tx_ico(self, ext: str, exact: bool = False) -> bool:
        self.permit_caching()
        if ext.endswith("/"):
            ext = "folder"
            exact = True

        bad = re.compile(r"[](){}/ []|^[0-9_-]*$")
        n = ext.split(".")[::-1]
        if not exact:
            n = n[:-1]

        ext = ""
        for v in n:
            if len(v) > 7 or bad.search(v):
                break

            ext = "{}.{}".format(v, ext)

        ext = ext.rstrip(".") or "unk"
        if len(ext) > 11:
            ext = "~" + ext[-9:]

        return self.tx_svg(ext, exact)

    def tx_svg(self, txt: str, small: bool = False) -> bool:
        # chrome cannot handle more than ~2000 unique SVGs
        # so url-param "raster" returns a png/webp instead
        # (useragent-sniffing kinshi due to caching proxies)
        mime, ico = self.ico.get(txt, not small, "raster" in self.uparam)

        lm = formatdate(self.E.t0)
        self.reply(ico, mime=mime, headers={"Last-Modified": lm})
        return True

    def tx_qr(self):
        url = "%s://%s%s%s" % (
            "https" if self.is_https else "http",
            self.host,
            self.args.SRS,
            self.vpaths,
        )
        uhash = ""
        uparams = []
        if self.ouparam:
            for k, v in self.ouparam.items():
                if k == "qr":
                    continue
                if k == "uhash":
                    uhash = v
                    continue
                uparams.append(k if v == "" else "%s=%s" % (k, v))
        if uparams:
            url += "?" + "&".join(uparams)
        if uhash:
            url += "#" + uhash

        self.log("qrcode(%r)" % (url,))
        ret = qr2svg(qrgen(url.encode("utf-8")), 2)
        self.reply(ret.encode("utf-8"), mime="image/svg+xml")
        return True

    def tx_md(self, vn: VFS, fs_path: str) -> bool:
        logmsg = "     %s @%s " % (self.req, self.uname)

        if not self.can_write:
            if "edit" in self.uparam or "edit2" in self.uparam:
                return self.tx_404(True)

        tpl = "mde" if "edit2" in self.uparam else "md"
        template = self.j2j(tpl)

        st = bos.stat(fs_path)
        ts_md = st.st_mtime

        max_sz = 1024 * self.args.txt_max
        sz_md = 0
        lead = b""
        fullfile = b""
        for buf in yieldfile(fs_path, self.args.iobuf):
            if sz_md < max_sz:
                fullfile += buf
            else:
                fullfile = b""

            if not sz_md and buf.startswith((b"\n", b"\r\n")):
                lead = b"\n" if buf.startswith(b"\n") else b"\r\n"
                sz_md += len(lead)

            sz_md += len(buf)
            for c, v in [(b"&", 4), (b"<", 3), (b">", 3)]:
                sz_md += (len(buf) - len(buf.replace(c, b""))) * v

        if (
            fullfile
            and "exp" in vn.flags
            and "edit" not in self.uparam
            and "edit2" not in self.uparam
            and vn.flags.get("exp_md")
        ):
            fulltxt = fullfile.decode("utf-8", "replace")
            fulltxt = self._expand(fulltxt, vn.flags.get("exp_md") or [])
            fullfile = fulltxt.encode("utf-8", "replace")

        if fullfile:
            fullfile = html_bescape(fullfile)
            sz_md = len(lead) + len(fullfile)

        file_ts = int(max(ts_md, self.E.t0))
        file_lastmod, do_send, _ = self._chk_lastmod(file_ts)
        self.out_headers["Last-Modified"] = file_lastmod
        self.out_headers["Cache-Control"] = "no-cache"
        status = 200 if do_send else 304

        arg_base = "?"
        if "k" in self.uparam:
            arg_base = "?k={}&".format(self.uparam["k"])

        boundary = "\roll\tide"
        targs = {
            "r": self.args.SR if self.is_vproxied else "",
            "ts": self.conn.hsrv.cachebuster(),
            "edit": "edit" in self.uparam,
            "title": html_escape(self.vpath, crlf=True),
            "lastmod": int(ts_md * 1000),
            "lang": self.cookies.get("cplng") or self.args.lang,
            "favico": self.args.favico,
            "have_emp": int(self.args.emp),
            "md_no_br": int(vn.flags.get("md_no_br") or 0),
            "md_chk_rate": self.args.mcr,
            "md": boundary,
            "arg_base": arg_base,
        }

        if self.args.js_other and "js" not in targs:
            zs = self.args.js_other
            zs += "&" if "?" in zs else "?"
            targs["js"] = zs

        if "html_head_d" in self.vn.flags:
            targs["this"] = self
            self._build_html_head(targs)

        targs["html_head"] = self.html_head
        zs = template.render(**targs).encode("utf-8", "replace")
        html = zs.split(boundary.encode("utf-8"))
        if len(html) != 2:
            raise Exception("boundary appears in " + tpl)

        self.send_headers("oh_g", sz_md + len(html[0]) + len(html[1]), status)

        logmsg += unicode(status)
        if self.mode == "HEAD" or not do_send:
            if self.do_log:
                self.log(logmsg)

            return True

        try:
            self.s.sendall(html[0] + lead)
            if fullfile:
                self.s.sendall(fullfile)
            else:
                for buf in yieldfile(fs_path, self.args.iobuf):
                    self.s.sendall(html_bescape(buf))

            self.s.sendall(html[1])

        except:
            self.log(logmsg + " \033[31md/c\033[0m")
            return False

        if self.do_log:
            self.log(logmsg + " " + unicode(len(html)))

        return True

    def tx_svcs(self) -> bool:
        aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a"
        ep = self.host
        sep = "]:" if "]" in ep else ":"
        if sep in ep:
            host, hport = ep.rsplit(":", 1)
            hport = ":" + hport
        else:
            host = ep
            hport = ""

        if host.endswith(".local") and self.args.zm and not self.args.rclone_mdns:
            rip = self.conn.hsrv.nm.map(self.ip) or host
            if ":" in rip and "[" not in rip:
                rip = "[%s]" % (rip,)
        else:
            rip = host

        defpw = "dave:hunter2" if self.args.usernames else "hunter2"

        vp = (self.uparam["hc"] or "").lstrip("/")
        pw = self.ouparam.get(self.args.pw_urlp) or defpw
        if pw in self.asrv.sesa:
            pw = defpw

        unpw = pw
        try:
            un, pw = unpw.split(":")
        except:
            un = ""
            if self.args.usernames:
                un = "dave"

        html = self.j2s(
            "svcs",
            args=self.args,
            accs=bool(self.asrv.acct),
            s="s" if self.is_https else "",
            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,
            b_un=("%s" % (html_sh_esc(un),)) if un else "k",
            un=html_sh_esc(un),
            pw=html_sh_esc(pw),
            unpw=html_sh_esc(unpw),
        )
        self.reply(html.encode("utf-8"))
        return True

    def tx_mounts(self) -> bool:
        suf = self.urlq({}, ["h"])
        rvol, wvol, avol = [
            [("/" + x).rstrip("/") + "/" for x in y]
            for y in [self.rvol, self.wvol, self.avol]
        ]
        for zs in self.asrv.vfs.all_fvols:
            if not zs:
                continue  # webroot
            zs2 = ("/" + zs).rstrip("/") + "/"
            for zsl in (rvol, wvol, avol):
                if zs2 in zsl:
                    zsl[zsl.index(zs2)] = zs2[:-1]

        ups = []
        now = time.time()
        get_vst = self.avol and not self.args.no_rescan
        get_ups = self.rvol and not self.args.no_up_list and self.uname or ""
        if get_vst or get_ups:
            x = self.conn.hsrv.broker.ask("up2k.get_state", get_vst, get_ups)
            vs = json.loads(x.get())
            vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
            try:
                for rem, sz, t0, poke, vp in vs["ups"]:
                    fdone = max(0.001, 1 - rem)
                    td = max(0.1, now - t0)
                    rd, fn = vsplit(vp.replace(os.sep, "/"))
                    if rd:
                        rds = rd.replace("/", " / ")
                        erd = "/%s/" % (quotep(rd),)
                    else:
                        erd = rds = "/"
                    spd = humansize(sz * fdone / td, True) + "/s"
                    eta = s2hms((td / fdone) - td, True) if rem < 1 else "--"
                    idle = s2hms(now - poke, True)
                    ups.append((int(100 * fdone), spd, eta, idle, erd, rds, fn))
            except Exception as ex:
                self.log("failed to list upload progress: %r" % (ex,), 1)
        if not get_vst:
            vstate = {}
            vs = {
                "scanning": None,
                "hashq": None,
                "tagq": None,
                "mtpq": None,
                "dbwt": None,
            }

        assert vstate is not None and vstate.items and vs  # type: ignore  # !rm

        dls = dl_list = []
        if self.conn.hsrv.tdls:
            zi = self.args.dl_list
            if zi == 2 or (zi == 1 and self.avol):
                dl_list = self.get_dls()
        for t0, t1, sent, sz, vp, dl_id, uname in dl_list:
            td = max(0.1, now - t0)
            rd, fn = vsplit(vp)
            if rd:
                rds = rd.replace("/", " / ")
                erd = "/%s/" % (quotep(rd),)
            else:
                erd = rds = "/"
            spd = humansize(sent / td, True) + "/s"
            hsent = humansize(sent, True)
            idle = s2hms(now - t1, True)
            usr = "%s @%s" % (dl_id, uname) if dl_id else uname
            if sz and sent and td:
                eta = s2hms((sz - sent) / (sent / td), True)
                perc = int(100 * sent / sz)
            else:
                eta = perc = "--"

            fn = html_escape(fn) if fn else self.conn.hsrv.iiam
            dls.append((perc, hsent, spd, eta, idle, usr, erd, rds, fn))

        if self.args.have_unlistc:
            allvols = self.asrv.vfs.all_nodes
            rvol = [x for x in rvol if "unlistcr" not in allvols[x.strip("/")].flags]
            wvol = [x for x in wvol if "unlistcw" not in allvols[x.strip("/")].flags]

        fmt = self.uparam.get("ls", "")
        if not fmt and self.ua.startswith(("curl/", "fetch")):
            fmt = "v"

        if fmt in ["v", "t", "txt"]:
            if self.uname == "*":
                txt = "howdy stranger (you're not logged in)"
            else:
                txt = "welcome back {}".format(self.uname)

            if vstate:
                txt += "\nstatus:"
                for k in ["scanning", "hashq", "tagq", "mtpq", "dbwt"]:
                    txt += " {}({})".format(k, vs[k])

            if ups:
                txt += "\n\nincoming files:"
                for zt in ups:
                    txt += "\n%s" % (", ".join((str(x) for x in zt)),)
                txt += "\n"

            if dls:
                txt += "\n\nactive downloads:"
                for zt in dls:
                    txt += "\n%s" % (", ".join((str(x) for x in zt)),)
                txt += "\n"

            if rvol:
                txt += "\nyou can browse:"
                for v in rvol:
                    txt += "\n  " + v

            if wvol:
                txt += "\nyou can upload to:"
                for v in wvol:
                    txt += "\n  " + v

            zb = txt.encode("utf-8", "replace") + b"\n"
            self.reply(zb, mime="text/plain; charset=utf-8")
            return True

        re_btn = ""
        nre = self.args.ctl_re
        if "re" in self.uparam:
            self.out_headers["Refresh"] = str(nre)
        elif nre:
            re_btn = "&re=%s" % (nre,)

        zi = self.args.ver_iwho
        show_ver = zi and (
            zi == 9 or (zi == 6 and self.uname != "*") or (zi == 3 and avol)
        )

        html = self.j2s(
            "splash",
            this=self,
            qvpath=quotep(self.vpaths) + self.ourlq(),
            rvol=rvol,
            wvol=wvol,
            avol=avol,
            in_shr=self.args.shr and self.vpath.startswith(self.args.shr1),
            vstate=vstate,
            dls=dls,
            ups=ups,
            scanning=vs["scanning"],
            hashq=vs["hashq"],
            tagq=vs["tagq"],
            mtpq=vs["mtpq"],
            dbwt=vs["dbwt"],
            url_suf=suf,
            re=re_btn,
            k304=self.k304(),
            no304=self.no304(),
            k304vis=self.args.k304 > 0,
            no304vis=self.args.no304 > 0,
            msg=(
                BADVER
                if self.conn.hsrv.bad_ver and avol
                else BADXFFB
                if hasattr(self, "bad_xff")
                else ""
            ),
            ver=S_VERSION if show_ver else "",
            chpw=self.args.chpw and self.uname != "*",
            ahttps="" if self.is_https else "https://" + self.host + self.req,
        )
        self.reply(html.encode("utf-8"))
        return True

    def setck(self) -> bool:
        zs = self.uparam["setck"]
        if len(zs) > 9 or RE_SETCK.search(zs):
            raise Pebkac(400, "illegal value")
        k, v = zs.split("=")
        t = 0 if v in ("", "x") else 86400 * 299
        ck = gencookie(k, v, self.args.R, True, False, t)
        self.out_headerlist.append(("Set-Cookie", ck))
        if "cc" in self.ouparam:
            self.redirect("", "?h#cc")
        else:
            self.reply(b"o7\n")
        return True

    def set_cfg_reset(self) -> bool:
        for k in ALL_COOKIES:
            if k not in self.cookies:
                continue
            cookie = gencookie(k, "x", self.args.R, True, False)
            self.out_headerlist.append(("Set-Cookie", cookie))

        self.redirect("", "?h#cc")
        return True

    def tx_404(self, is_403: bool = False) -> bool:
        rc = 404
        if self.args.vague_403:
            t = '

404 not found  ┐( ´ -`)┌

or maybe you don\'t have access -- try a password or go home

' pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try a password)" elif is_403: t = '

403 forbiddena  ~┻━┻

use a password or go home

' pt = "403 forbiddena ~┻━┻ (you'll have to log in)" rc = 403 else: t = '

404 not found  ┐( ´ -`)┌

go home

' pt = "404 not found ┐( ´ -`)┌" if self.ua.startswith(("curl/", "fetch")): pt = "# acct: %s\n%s\n" % (self.uname, pt) self.reply(pt.encode("utf-8"), status=rc) return True if "th" in self.ouparam and str(self.ouparam["th"])[:1] in "jw": return self.tx_svg("e" + pt[:3]) # most webdav clients will not send credentials until they # get 401'd, so send a challenge if we're Absolutely Sure # that the client is not a graphical browser if rc == 403 and self.uname == "*": sport = self.s.getsockname()[1] if self.args.dav_port == sport or ( "sec-fetch-site" not in self.headers and self.cookies.get("js") != "y" and sport not in self.args.p_nodav and ( not self.args.ua_nodav.search(self.ua) or (self.args.dav_ua1 and self.args.dav_ua1.search(self.ua)) ) ): rc = 401 self.out_headers["WWW-Authenticate"] = 'Basic realm="a"' t = t.format(self.args.SR) qv = quotep(self.vpaths) + self.ourlq() html = self.j2s( "splash", this=self, qvpath=qv, msg=t, in_shr=self.args.shr and self.vpath.startswith(self.args.shr1), ahttps="" if self.is_https else "https://" + self.host + self.req, ) self.reply(html.encode("utf-8"), status=rc) return True def on40x(self, mods: list[str], vn: VFS, rem: str) -> str: for mpath in mods: try: mod = loadpy(mpath, self.args.hot_handlers) except Exception as ex: self.log("import failed: {!r}".format(ex)) continue ret = mod.main(self, vn, rem) if ret: return ret.lower() return "" # unhandled / fallthrough def scanvol(self) -> bool: if self.args.no_rescan: raise Pebkac(403, "the rescan feature is disabled in server config") vpaths = self.uparam["scan"].split(",/") if vpaths == [""]: vpaths = [self.vpath] vols = [] for vpath in vpaths: vn, _ = self.asrv.vfs.get(vpath, self.uname, True, True) vols.append(vn.vpath) if self.uname not in vn.axs.uadmin: self.log("rejected scanning [%s] => [%s];" % (vpath, vn.vpath), 3) raise Pebkac(403, "'scanvol' not allowed for user " + self.uname) self.log("trying to rescan %d volumes: %r" % (len(vols), vols)) args = [self.asrv.vfs.all_vols, vols, False, True] x = self.conn.hsrv.broker.ask("up2k.rescan", *args) err = x.get() if not err: self.redirect("", "?h") return True raise Pebkac(500, err) def handle_reload(self) -> bool: act = self.uparam.get("reload") if act != "cfg": raise Pebkac(400, "only config files ('cfg') can be reloaded rn") if not self.avol: raise Pebkac(403, "'reload' not allowed for user " + self.uname) if self.args.no_reload: raise Pebkac(403, "the reload feature is disabled in server config") x = self.conn.hsrv.broker.ask("reload", True, True) return self.redirect("", "?h", x.get(), "return to", False) def tx_stack(self) -> bool: zs = self.args.stack_who if zs == "all" or ( (zs == "a" and self.avol) or (zs == "rw" and [x for x in self.wvol if x in self.rvol]) ): pass else: raise Pebkac(403, "'stack' not allowed for user " + self.uname) ret = html_escape(alltrace(self.args.stack_v)) if self.args.stack_v: ret = "
%s\n%s" % (time.time(), ret)
        else:
            ret = "
%s" % (ret,)
        self.reply(ret.encode("utf-8"))
        return True

    def tx_tree(self) -> bool:
        top = self.uparam["tree"] or ""
        dst = self.vpath
        if top in [".", ".."]:
            top = undot(self.vpath + "/" + top)

        if top == dst:
            dst = ""
        elif top:
            if not dst.startswith(top + "/"):
                raise Pebkac(422, "arg funk")

            dst = dst[len(top) + 1 :]

        ret = self.gen_tree(top, dst, self.uparam.get("k", ""))
        if self.is_vproxied and not self.uparam["tree"]:
            # uparam is '' on initial load, which is
            # the only time we gotta fill in the blanks
            parents = self.args.R.split("/")
            for parent in reversed(parents):
                ret = {"k%s" % (parent,): ret, "a": []}

        zs = json.dumps(ret)
        self.reply(zs.encode("utf-8"), mime="application/json")
        return True

    def gen_tree(self, top: str, target: str, dk: str) -> dict[str, Any]:
        ret: dict[str, Any] = {}
        excl = None
        if target:
            excl, target = (target.split("/", 1) + [""])[:2]
            sub = self.gen_tree("/".join([top, excl]).strip("/"), target, dk)
            ret["k" + quotep(excl)] = sub

        vfs = self.asrv.vfs
        dk_sz = False
        if dk:
            vn, rem = vfs.get(top, self.uname, False, False)
            if vn.flags.get("dks") and self._use_dirkey(vn, vn.canonical(rem)):
                dk_sz = vn.flags.get("dk")

        dots = False
        fsroot = ""
        try:
            vn, rem = vfs.get(top, self.uname, not dk_sz, False)
            fsroot, vfs_ls, vfs_virt = vn.ls(
                rem,
                self.uname,
                not self.args.no_scandir,
                PERMS_rwh,
            )
            dots = self.uname in vn.axs.udot and "dots" in self.uparam
            dk_sz = vn.flags.get("dk")
        except:
            dk_sz = None
            vfs_ls = []
            vfs_virt = {}
            for v in self.rvol:
                d1, d2 = v.rsplit("/", 1) if "/" in v else ["", v]
                if d1 == top:
                    vfs_virt[d2] = vfs  # typechk, value never read

        dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]

        if not dots:
            dirs = exclude_dotfiles(dirs)

        dirs = [quotep(x) for x in dirs if x != excl]

        if dk_sz and fsroot:
            kdirs = []
            fsroot_ = os.path.join(fsroot, "")
            for dn in dirs:
                ap = fsroot_ + dn
                zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz]
                kdirs.append(dn + "?k=" + zs)
            dirs = kdirs

        if vfs_virt:
            for x in vfs_virt:
                if x == excl:
                    continue
                try:
                    dvn, drem = vfs.get(vjoin(top, x), self.uname, False, False)
                    if (
                        self.uname not in dvn.axs.uread
                        and self.uname not in dvn.axs.uwrite
                        and self.uname not in dvn.axs.uhtml
                    ):
                        raise Exception()
                    bos.stat(dvn.canonical(drem, False))
                except:
                    x += "\n"
                dirs.append(quotep(x))
            if not dots:
                dirs = exclude_dotfiles(dirs)

        ret["a"] = dirs
        return ret

    def get_dls(self) -> list[list[Any]]:
        ret = []
        dls = self.conn.hsrv.tdls
        enshare = self.args.shr
        shrs = enshare[1:]
        for dl_id, (t0, sz, vn, vp, uname) in self.conn.hsrv.tdli.items():
            t1, sent = dls[dl_id]
            if sent > 0x100000:  # 1m; buffers 2~4
                sent -= 0x100000
            if self.uname not in vn.axs.uread:
                vp = ""
            elif self.uname not in vn.axs.udot and (vp.startswith(".") or "/." in vp):
                vp = ""
            elif (
                enshare
                and vp.startswith(shrs)
                and self.uname != vn.shr_owner
                and self.uname not in vn.axs.uadmin
                and self.uname not in self.args.shr_adm
                and not dl_id.startswith(self.ip + ":")
            ):
                vp = ""
            if self.uname not in vn.axs.uadmin:
                dl_id = uname = ""

            ret.append([t0, t1, sent, sz, vp, dl_id, uname])
        return ret

    def tx_dls(self) -> bool:
        ret = [
            {
                "t0": x[0],
                "t1": x[1],
                "sent": x[2],
                "size": x[3],
                "path": x[4],
                "conn": x[5],
                "uname": x[6],
            }
            for x in self.get_dls()
        ]
        zs = json.dumps(ret, separators=(",\n", ": "))
        self.reply(zs.encode("utf-8", "replace"), mime="application/json")
        return True

    def tx_ups(self) -> bool:
        idx = self.conn.get_u2idx()
        if not idx or not hasattr(idx, "p_end"):
            if not HAVE_SQLITE3:
                raise Pebkac(500, "sqlite3 not found on server; unpost is disabled")
            raise Pebkac(500, "server busy, cannot unpost; please retry in a bit")

        sfilt = self.uparam.get("filter") or ""
        nfi, vfi = str_anchor(sfilt)
        lm = "ups %d%r" % (nfi, sfilt)

        if self.args.shr and self.vpath.startswith(self.args.shr1):
            shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
        else:
            shr_dbv = None

        wret: dict[str, Any] = {}
        ret: list[dict[str, Any]] = []
        t0 = time.time()
        lim = time.time() - self.args.unpost
        fk_vols = {
            vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
            for vp, vol in self.asrv.vfs.all_vols.items()
            if "fk" in vol.flags
            and (self.uname in vol.axs.uread or self.uname in vol.axs.upget)
        }

        if hasattr(self, "bad_xff"):
            allvols = []
            t = "will not return list of recent uploads" + BADXFF
            self.log(t, 1)
            if self.avol:
                raise Pebkac(500, t)

            x = self.conn.hsrv.broker.ask("up2k.get_unfinished_by_user", self.uname, "")
        else:
            x = self.conn.hsrv.broker.ask(
                "up2k.get_unfinished_by_user", self.uname, self.ip
            )
        zdsa: dict[str, Any] = x.get()
        uret: list[dict[str, Any]] = []
        if "timeout" in zdsa:
            wret["nou"] = 1
        else:
            uret = zdsa["f"]
        nu = len(uret)

        if not self.args.unpost:
            allvols = []
        else:
            allvols = list(self.asrv.vfs.all_vols.values())

        allvols = [
            x
            for x in allvols
            if "e2d" in x.flags
            and ("*" in x.axs.uwrite or self.uname in x.axs.uwrite or x == shr_dbv)
        ]

        q = ""
        qp = (0,)
        q_c = -1

        for vol in allvols:
            cur = idx.get_cur(vol)
            if not cur:
                continue

            nfk, fk_alg = fk_vols.get(vol) or (0, 0)

            zi = vol.flags["unp_who"]
            if q_c != zi:
                q_c = zi
                q = "select sz, rd, fn, at from up where "
                if zi == 1:
                    q += "ip=? and un=?"
                    qp = (self.ip, self.uname, lim)
                elif zi == 2:
                    q += "ip=?"
                    qp = (self.ip, lim)
                if zi == 3:
                    q += "un=?"
                    qp = (self.uname, lim)
                q += " and at>? order by at desc"

            n = 2000
            for sz, rd, fn, at in cur.execute(q, qp):
                vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
                if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
                    pass
                elif nfi == 2:
                    if not vp.lower().startswith(vfi):
                        continue
                elif nfi == 3:
                    if not vp.lower().endswith(vfi):
                        continue
                else:
                    continue

                n -= 1
                if not n:
                    break

                rv = {"vp": vp, "sz": sz, "at": at, "nfk": nfk}
                if nfk:
                    rv["ap"] = vol.canonical(vjoin(rd, fn))
                    rv["fk_alg"] = fk_alg

                ret.append(rv)
                if len(ret) > 3000:
                    ret.sort(key=lambda x: x["at"], reverse=True)  # type: ignore
                    ret = ret[:2000]

        ret.sort(key=lambda x: x["at"], reverse=True)  # type: ignore

        if len(ret) > 2000:
            ret = ret[:2000]
        if len(ret) >= 2000:
            wret["oc"] = 1

        for rv in ret:
            rv["vp"] = quotep(rv["vp"])
            nfk = rv.pop("nfk")
            if not nfk:
                continue

            alg = rv.pop("fk_alg")
            ap = rv.pop("ap")
            try:
                st = bos.stat(ap)
            except:
                continue

            fk = self.gen_fk(
                alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
            )
            rv["vp"] += "?k=" + fk[:nfk]

        if not allvols:
            wret["noc"] = 1
            ret = []

        nc = len(ret)
        ret = uret + ret

        if shr_dbv:
            # translate vpaths from share-target to share-url
            # to satisfy access checks
            assert shr_vrem is not None and shr_vrem.split  # type: ignore  # !rm
            vp_shr, vp_vfs = vroots(self.vpath, vjoin(shr_dbv.vpath, shr_vrem))
            for v in ret:
                vp = v["vp"]
                if vp.startswith(vp_vfs):
                    v["vp"] = vp_shr + vp[len(vp_vfs) :]

        if self.is_vproxied:
            for v in ret:
                v["vp"] = self.args.SR + v["vp"]

        wret["f"] = ret
        wret["nu"] = nu
        wret["nc"] = nc
        jtxt = json.dumps(wret, separators=(",\n", ": "))
        self.log("%s #%d+%d %.2fsec" % (lm, nu, nc, time.time() - t0))
        self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
        return True

    def tx_rups(self) -> bool:
        if self.args.no_ups_page:
            raise Pebkac(500, "listing of recent uploads is disabled in server config")

        idx = self.conn.get_u2idx()
        if not idx or not hasattr(idx, "p_end"):
            if not HAVE_SQLITE3:
                raise Pebkac(500, "sqlite3 not found on server; recent-uploads n/a")
            raise Pebkac(500, "server busy, cannot list recent uploads; please retry")

        sfilt = self.uparam.get("filter") or ""
        nfi, vfi = str_anchor(sfilt)
        lm = "ru %d%r" % (nfi, sfilt)
        self.log(lm)

        ret: list[dict[str, Any]] = []
        t0 = time.time()
        allvols = [
            x
            for x in self.asrv.vfs.all_vols.values()
            if "e2d" in x.flags and ("*" in x.axs.uread or self.uname in x.axs.uread)
        ]
        fk_vols = {
            vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
            for vol in allvols
            if "fk" in vol.flags and "*" not in vol.axs.uread
        }

        for vol in allvols:
            cur = idx.get_cur(vol)
            if not cur:
                continue

            nfk, fk_alg = fk_vols.get(vol) or (0, 0)
            adm = "*" in vol.axs.uadmin or self.uname in vol.axs.uadmin
            dots = "*" in vol.axs.udot or self.uname in vol.axs.udot

            lvl = vol.flags["ups_who"]
            if not lvl:
                continue
            elif lvl == 1 and not adm:
                continue

            n = 1000
            q = "select sz, rd, fn, ip, at, un from up where at>0 order by at desc"
            for sz, rd, fn, ip, at, un in cur.execute(q):
                vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
                if nfi == 0 or (nfi == 1 and vfi in vp.lower()):
                    pass
                elif nfi == 2:
                    if not vp.lower().startswith(vfi):
                        continue
                elif nfi == 3:
                    if not vp.lower().endswith(vfi):
                        continue
                else:
                    continue

                if not dots and "/." in vp:
                    continue

                rv = {
                    "vp": vp,
                    "sz": sz,
                    "ip": ip,
                    "at": at,
                    "un": un,
                    "nfk": nfk,
                    "adm": adm,
                }
                if nfk:
                    rv["ap"] = vol.canonical(vjoin(rd, fn))
                    rv["fk_alg"] = fk_alg

                ret.append(rv)
                if len(ret) > 2000:
                    ret.sort(key=lambda x: x["at"], reverse=True)  # type: ignore
                    ret = ret[:1000]

                n -= 1
                if not n:
                    break

        ret.sort(key=lambda x: x["at"], reverse=True)  # type: ignore

        if len(ret) > 1000:
            ret = ret[:1000]

        for rv in ret:
            rv["vp"] = quotep(rv["vp"])
            nfk = rv.pop("nfk")
            if not nfk:
                continue

            alg = rv.pop("fk_alg")
            ap = rv.pop("ap")
            try:
                st = bos.stat(ap)
            except:
                continue

            fk = self.gen_fk(
                alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
            )
            rv["vp"] += "?k=" + fk[:nfk]

        if self.args.ups_when:
            for rv in ret:
                adm = rv.pop("adm")
                if not adm:
                    rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
                    if rv["un"] not in ("*", self.uname):
                        rv["un"] = "(?)"
        else:
            for rv in ret:
                adm = rv.pop("adm")
                if not adm:
                    rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
                    rv["at"] = 0
                    if rv["un"] not in ("*", self.uname):
                        rv["un"] = "(?)"

        if self.is_vproxied:
            for v in ret:
                v["vp"] = self.args.SR + v["vp"]

        now = time.time()
        self.log("%s #%d %.2fsec" % (lm, len(ret), now - t0))

        ret2 = {"now": int(now), "filter": sfilt, "ups": ret}
        jtxt = json.dumps(ret2, separators=(",\n", ": "))
        if "j" in self.ouparam:
            self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
            return True

        html = self.j2s("rups", this=self, v=json_hesc(jtxt))
        self.reply(html.encode("utf-8"), status=200)
        return True

    def tx_idp(self) -> bool:
        if self.uname.lower() not in self.args.idp_adm_set:
            raise Pebkac(403, "'idp' not allowed for user " + self.uname)

        cmd = self.uparam["idp"]
        if cmd.startswith("rm="):
            import sqlite3

            db = sqlite3.connect(self.args.idp_db)
            db.execute("delete from us where un=?", (cmd[3:],))
            db.commit()
            db.close()

            self.conn.hsrv.broker.ask("reload", False, True).get()

            self.redirect("", "?idp")
            return True

        rows = [
            [k, "[%s]" % ("], [".join(v))]
            for k, v in sorted(self.asrv.idp_accs.items())
        ]
        html = self.j2s("idp", this=self, rows=rows, now=int(time.time()))
        self.reply(html.encode("utf-8"), status=200)
        return True

    def tx_shares(self) -> bool:
        if self.uname == "*":
            self.loud_reply("you're not logged in")
            return True

        idx = self.conn.get_u2idx()
        if not idx or not hasattr(idx, "p_end"):
            if not HAVE_SQLITE3:
                raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
            raise Pebkac(500, "server busy, cannot list shares; please retry in a bit")

        cur = idx.get_shr()
        if not cur:
            raise Pebkac(400, "huh, sharing must be disabled in the server config...")

        rows = cur.execute("select * from sh").fetchall()
        rows = [list(x) for x in rows]

        if self.uname != self.args.shr_adm:
            rows = [x for x in rows if x[5] == self.uname]

        q = "select vp from sf where k=? limit 99"
        for r in rows:
            if not r[4]:
                r[4] = "---"
            else:
                zstl = cur.execute(q, (r[0],)).fetchall()
                zsl = [html_escape(zst[0]) for zst in zstl]
                r[4] = "
".join(zsl) if self.args.shr_site: site = self.args.shr_site[:-1] else: site = "" if self.is_vproxied: site += self.args.SR html = self.j2s( "shares", this=self, shr=self.args.shr, site=site, rows=rows, now=int(time.time()), ) self.reply(html.encode("utf-8"), status=200) return True def handle_eshare(self) -> bool: idx = self.conn.get_u2idx() if not idx or not hasattr(idx, "p_end"): if not HAVE_SQLITE3: 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: " + skey) cur = idx.get_shr() if not cur: raise Pebkac(400, "huh, sharing must be disabled in the server config...") rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall() un = rows[0][0] if rows and rows[0] else "" if not un: raise Pebkac(400, "that sharekey didn't match anything") expiry = rows[0][1] if un != self.uname and self.uname != self.args.shr_adm: t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin" raise Pebkac(400, t % (self.uname, un)) reload = False act = self.uparam["eshare"] if act == "rm": cur.execute("delete from sh where k = ?", (skey,)) if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes: reload = True else: now = time.time() if expiry < now: expiry = now reload = True expiry += int(act) * 60 cur.execute("update sh set t1 = ? where k = ?", (expiry, skey)) cur.connection.commit() if reload: self.conn.hsrv.broker.ask("reload", False, True).get() self.conn.hsrv.broker.ask("up2k.wake_rescanner").get() self.redirect("", "?shares") return True def handle_share(self, req: dict[str, str]) -> bool: idx = self.conn.get_u2idx() if not idx or not hasattr(idx, "p_end"): if not HAVE_SQLITE3: raise Pebkac(500, "sqlite3 not found on server; sharing is disabled") raise Pebkac(500, "server busy, cannot create share; please retry in a bit") if self.args.shr_v: self.log("handle_share: " + json.dumps(req, indent=4)) skey = req["k"] vps = req["vp"] fns = [] if len(vps) == 1: vp = vps[0] if not vp.endswith("/"): vp, zs = vp.rsplit("/", 1) fns = [zs] else: for zs in vps: if zs.endswith("/"): t = "you cannot select more than one folder, or mix files and folders in one selection" raise Pebkac(400, t) vp = vps[0].rsplit("/", 1)[0] for zs in vps: vp2, fn = zs.rsplit("/", 1) fns.append(fn) if vp != vp2: t = "mismatching base paths in selection:\n %r\n %r" raise Pebkac(400, t % (vp, vp2)) vp = vp.strip("/") if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)): vp = vp[len(self.args.RS) :] m = re.search(r"([^0-9a-zA-Z_-])", skey) if m: raise Pebkac(400, "sharekey has illegal character %r" % (m[1],)) if vp.startswith(self.args.shr1): raise Pebkac(400, "yo dawg...") cur = idx.get_shr() if not cur: raise Pebkac(400, "huh, sharing must be disabled in the server config...") q = "select * from sh where k = ?" qr = cur.execute(q, (skey,)).fetchall() if qr and qr[0]: self.log("sharekey taken by %r" % (qr,)) raise Pebkac(400, "sharekey %r is already in use" % (skey,)) # ensure user has requested perms s_rd = "read" in req["perms"] s_wr = "write" in req["perms"] s_get = "get" in req["perms"] s_axs = [s_rd, s_wr, False, False, s_get] if s_axs == [False] * 5: raise Pebkac(400, "select at least one permission") try: vfs, rem = self.asrv.vfs.get(vp, self.uname, *s_axs) except: raise Pebkac(400, "you dont have all the perms you tried to grant") zs = vfs.flags["shr_who"] if zs == "auth" and self.uname != "*": pass elif zs == "a" and self.uname in vfs.axs.uadmin: pass else: raise Pebkac(400, "you dont have perms to create shares from this volume") ap, reals, _ = vfs.ls(rem, self.uname, not self.args.no_scandir, [s_axs]) rfns = set([x[0] for x in reals]) for fn in fns: if fn not in rfns: raise Pebkac(400, "selected file not found on disk: %r" % (fn,)) pw = req.get("pw") or "" pw = self.asrv.ah.hash(pw) now = int(time.time()) sexp = req["exp"] exp = int(sexp) if sexp else 0 exp = now + exp * 60 if exp else 0 pr = "".join(zc for zc, zb in zip("rwmdg", s_axs) if zb) q = "insert into sh values (?,?,?,?,?,?,?,?)" cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp)) q = "insert into sf values (?,?)" for fn in fns: cur.execute(q, (skey, fn)) cur.connection.commit() self.conn.hsrv.broker.ask("reload", False, True).get() self.conn.hsrv.broker.ask("up2k.wake_rescanner").get() fn = quotep(fns[0]) if len(fns) == 1 else "" # NOTE: several clients (frontend, party-up) expect url at response[15:] if self.args.shr_site: surl = "created share: %s%s%s%s/%s" % ( self.args.shr_site[:-1], self.args.SR, self.args.shr, skey, fn, ) else: surl = "created share: %s://%s%s%s%s/%s" % ( "https" if self.is_https else "http", self.host, self.args.SR, self.args.shr, skey, fn, ) self.loud_reply(surl, status=201) return True def handle_rm(self, req: list[str]) -> bool: if not req and not self.can_delete: if self.mode == "DELETE" and self.uname == "*": raise Pebkac(401, "authenticate") # webdav raise Pebkac(403, "'delete' not allowed for user " + self.uname) if self.args.no_del: raise Pebkac(403, "the delete feature is disabled in server config") unpost = "unpost" in self.uparam if unpost and hasattr(self, "bad_xff"): self.log("unpost was denied" + BADXFF, 1) raise Pebkac(403, "the delete feature is disabled in server config") if not unpost and self.vn.shr_src: raise Pebkac(403, "files in shares can only be deleted with unpost") if not req: req = [self.vpath] elif self.is_vproxied: req = [x[len(self.args.SR) :] for x in req] nlim = int(self.uparam.get("lim") or 0) lim = [nlim, nlim] if nlim else [] x = self.conn.hsrv.broker.ask( "up2k.handle_rm", self.uname, self.ip, req, lim, False, unpost ) self.loud_reply(x.get()) return True def handle_mv(self) -> bool: # full path of new loc (incl filename) dst = self.uparam.get("move") if self.is_vproxied and dst and dst.startswith(self.args.SR): dst = dst[len(self.args.RS) :] if not dst: raise Pebkac(400, "need dst vpath") return self._mv(self.vpath, dst.lstrip("/"), False) def _mv(self, vsrc: str, vdst: str, overwrite: bool) -> bool: if self.args.no_mv: raise Pebkac(403, "the rename/move feature is disabled in server config") # `handle_cpmv` will catch 403 from these and raise 401 svn, srem = self.asrv.vfs.get(vsrc, self.uname, True, False, True) dvn, drem = self.asrv.vfs.get(vdst, self.uname, False, True) if overwrite: dabs = dvn.canonical(drem) if bos.path.exists(dabs): self.log("overwriting %s" % (dabs,)) self.asrv.vfs.get(vdst, self.uname, False, True, False, True) wunlink(self.log, dabs, dvn.flags) x = self.conn.hsrv.broker.ask( "up2k.handle_mv", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst ) self.loud_reply(x.get(), status=201) return True def handle_cp(self) -> bool: # full path of new loc (incl filename) dst = self.uparam.get("copy") if self.is_vproxied and dst and dst.startswith(self.args.SR): dst = dst[len(self.args.RS) :] if not dst: raise Pebkac(400, "need dst vpath") return self._cp(self.vpath, dst.lstrip("/"), False) def _cp(self, vsrc: str, vdst: str, overwrite: bool) -> bool: if self.args.no_cp: raise Pebkac(403, "the copy feature is disabled in server config") svn, srem = self.asrv.vfs.get(vsrc, self.uname, True, False) dvn, drem = self.asrv.vfs.get(vdst, self.uname, False, True) if overwrite: dabs = dvn.canonical(drem) if bos.path.exists(dabs): self.log("overwriting %s" % (dabs,)) self.asrv.vfs.get(vdst, self.uname, False, True, False, True) wunlink(self.log, dabs, dvn.flags) x = self.conn.hsrv.broker.ask( "up2k.handle_cp", self.ouparam.get("akey"), self.uname, self.ip, vsrc, vdst ) self.loud_reply(x.get(), status=201) return True def handle_fs_abrt(self): if self.args.no_fs_abrt: t = "aborting an ongoing copy/move is disabled in server config" raise Pebkac(403, t) self.conn.hsrv.broker.say("up2k.handle_fs_abrt", self.uparam["fs_abrt"]) self.loud_reply("aborting", status=200) return True def tx_ls_vols(self) -> bool: e_d = {} eses = ["", ""] rvol = self.rvol wvol = self.wvol allvols = self.asrv.vfs.all_nodes if self.args.have_unlistc: rvol = [x for x in rvol if "unlistcr" not in allvols[x].flags] wvol = [x for x in wvol if "unlistcw" not in allvols[x].flags] vols = [(x, allvols[x]) for x in list(set(rvol + wvol))] if self.vpath: zs = "%s/" % (self.vpath,) vols = [(x[len(zs) :], y) for x, y in vols if x.startswith(zs)] vols = [(x.split("/", 1)[0], y) for x, y in vols] vols = list(({x: y for x, y in vols if x}).items()) if not vols and self.vpath: return self.tx_404(True) dirs = [ { "lead": "", "href": "%s/" % (x,), "ext": "---", "sz": 0, "ts": 0, "tags": e_d, "dt": 0, "name": 0, "perms": vn.get_perms("", self.uname), } for x, vn in sorted(vols) ] ls = { "dirs": dirs, "files": [], "acct": self.uname, "perms": [], "taglist": [], "logues": eses, "readmes": eses, "srvinf": "" if self.args.nih else self.args.name, } return self.tx_ls(ls) def tx_ls(self, ls: dict[str, Any]) -> bool: dirs = ls["dirs"] files = ls["files"] arg = self.uparam["ls"] if arg in ["v", "t", "txt"]: try: biggest = max(ls["files"] + ls["dirs"], key=itemgetter("sz"))["sz"] except: biggest = 0 if arg == "v": fmt = "\033[0;7;36m{{}}{{:>{}}}\033[0m {{}}" nfmt = "{}" biggest = 0 f2 = "".join( "{}{{}}".format(x) for x in [ "\033[7m", "\033[27m", "", "\033[0;1m", "\033[0;36m", "\033[0m", ] ) ctab = {"B": 6, "K": 5, "M": 1, "G": 3} for lst in [dirs, files]: for x in lst: a = x["dt"].replace("-", " ").replace(":", " ").split(" ") x["dt"] = f2.format(*list(a)) sz = humansize(x["sz"], True) x["sz"] = "\033[0;3{}m {:>5}".format(ctab.get(sz[-1:], 0), sz) else: fmt = "{{}} {{:{},}} {{}}" nfmt = "{:,}" for x in dirs: n = x["name"] + "/" if arg == "v": n = "\033[94m" + n x["name"] = n fmt = fmt.format(len(nfmt.format(biggest))) retl = [ ("# %s: %s" % (x, ls[x])).replace(r" // ", " // ") for x in ["acct", "perms", "srvinf"] if x in ls ] retl += [ fmt.format(x["dt"], x["sz"], x["name"]) for y in [dirs, files] for x in y ] ret = "\n".join(retl) mime = "text/plain; charset=utf-8" else: [x.pop(k) for k in ["name", "dt"] for y in [dirs, files] for x in y] # nonce (tlnote: norwegian for flake as in snowflake) if self.args.no_fnugg: ls["fnugg"] = "nei" elif "fnugg" in self.headers: ls["fnugg"] = self.headers["fnugg"] ret = json.dumps(ls) mime = "application/json" ret += "\n\033[0m" if arg == "v" else "\n" self.reply(ret.encode("utf-8", "replace"), mime=mime) return True def tx_browser(self) -> bool: vpath = "" vpnodes = [["", "/"]] if self.vpath: for node in self.vpath.split("/"): if not vpath: vpath = node else: vpath += "/" + node vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)]) vn = self.vn rem = self.rem abspath = vn.dcanonical(rem) dbv, vrem = vn.get_dbv(rem) try: st = bos.stat(abspath) except: if "on404" not in vn.flags: return self.tx_404(not self.can_read) ret = self.on40x(vn.flags["on404"], vn, rem) if ret == "true": return True elif ret == "false": return False elif ret == "retry": try: st = bos.stat(abspath) except: return self.tx_404(not self.can_read) else: return self.tx_404(not self.can_read) if rem.startswith(".hist/up2k.") or ( rem.endswith("/dir.txt") and rem.startswith(".hist/th/") ): raise Pebkac(403) e2d = "e2d" in vn.flags e2t = "e2t" in vn.flags add_og = "og" in vn.flags if add_og: if "th" in self.uparam or "raw" in self.uparam or "opds" in self.uparam: add_og = False elif vn.flags["og_ua"]: add_og = vn.flags["og_ua"].search(self.ua) og_fn = "" if "v" in self.uparam: add_og = True og_fn = "" if "b" in self.uparam and "norobots" not in vn.flags: self.out_headers["X-Robots-Tag"] = "noindex, nofollow" is_dir = stat.S_ISDIR(st.st_mode) is_dk = False fk_pass = False icur = None if (e2t or e2d) and (is_dir or add_og): idx = self.conn.get_u2idx() if idx and hasattr(idx, "p_end"): icur = idx.get_cur(dbv) if "k" in self.uparam or "dky" in vn.flags: if is_dir: use_dirkey = self._use_dirkey(vn, abspath) use_filekey = False else: use_filekey = self._use_filekey(vn, abspath, st) use_dirkey = False else: use_dirkey = use_filekey = False th_fmt = self.uparam.get("th") if self.can_read or ( self.can_get and (use_filekey or use_dirkey or (not is_dir and "fk" not in vn.flags)) ): if th_fmt is not None: nothumb = "dthumb" in dbv.flags if is_dir: vrem = vrem.rstrip("/") if nothumb: pass elif icur and vrem: q = "select fn from cv where rd=? and dn=?" crd, cdn = vrem.rsplit("/", 1) if "/" in vrem else ("", vrem) # no mojibake support: try: cfn = icur.execute(q, (crd, cdn)).fetchone() if cfn: fn = cfn[0] fp = os.path.join(abspath, fn) st = bos.stat(fp) vrem = "{}/{}".format(vrem, fn).strip("/") is_dir = False except: pass else: for fn in self.args.th_covers: fp = os.path.join(abspath, fn) try: st = bos.stat(fp) vrem = "{}/{}".format(vrem, fn).strip("/") is_dir = False break except: pass if is_dir: return self.tx_svg("folder") thp = None if self.thumbcli and not nothumb: try: thp = self.thumbcli.get(dbv, vrem, int(st.st_mtime), th_fmt) except Pebkac as ex: if ex.code == 500 and th_fmt[:1] in "jw": self.log("failed to convert [%s]:\n%s" % (abspath, ex), 3) return self.tx_svg("--error--\ncheck\nserver\nlog") raise if thp: return self.tx_file("oh_f", thp) if th_fmt == "p": raise Pebkac(404) elif th_fmt in ACODE2_FMT: raise Pebkac(415) return self.tx_ico(rem) elif self.can_write and th_fmt is not None: return self.tx_svg("upload\nonly") if not self.can_read and self.can_get and self.avn: if not self.can_html: pass elif is_dir: for fn in ("index.htm", "index.html"): ap2 = os.path.join(abspath, fn) try: st2 = bos.stat(ap2) except: continue # might as well be extra careful if not stat.S_ISREG(st2.st_mode): continue if not self.trailing_slash: return self.redirect( self.vpath + "/", flavor="redirecting to", use302=True ) fk_pass = True is_dir = False add_og = False rem = vjoin(rem, fn) vrem = vjoin(vrem, fn) abspath = ap2 break elif self.vpath.rsplit("/", 1)[-1] in IDX_HTML: fk_pass = True if not is_dir and (self.can_read or self.can_get): if ( not self.can_read and not fk_pass and "fk" in vn.flags and not use_filekey and not self.vpath.startswith(self.args.shr1 or "\n") ): return self.tx_404(True) is_md = abspath.lower().endswith(".md") if add_og and not is_md: if self.host not in self.headers.get("referer", ""): self.vpath, og_fn = vsplit(self.vpath) vpath = self.vpath vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) abspath = vn.dcanonical(rem) dbv, vrem = vn.get_dbv(rem) is_dir = stat.S_ISDIR(st.st_mode) is_dk = True vpnodes.pop() if ( ( (is_md and "v" in self.uparam) or "edit" in self.uparam or "edit2" in self.uparam ) and "nohtml" not in vn.flags and ( is_md or self.can_delete or ( "." in abspath and abspath.rsplit(".", 1)[1].lower() in vn.flags["rw_edit_set"] ) ) ): return self.tx_md(vn, abspath) if "zls" in self.uparam: return self.tx_zls(abspath) if "zget" in self.uparam: return self.tx_zget(abspath) if not add_og or not og_fn: if st.st_size or "nopipe" in vn.flags: return self.tx_file("oh_f", abspath, None) else: return self.tx_file("oh_f", abspath, vn.get_dbv("")[0].realpath) elif is_dir and not self.can_read: if use_dirkey: is_dk = True elif self.can_get and "doc" in self.uparam: zs = vjoin(self.vpath, self.uparam["doc"]) + "?v" return self.redirect(zs, flavor="redirecting to", use302=True) elif not self.can_write: return self.tx_404(True) srv_info = [] try: if not self.args.nih: srv_info.append(self.args.name_html) except: self.log("#wow #whoa") zi = vn.flags["du_iwho"] if zi and ( zi == 9 or (zi == 7 and self.uname != "*") or (zi == 5 and self.can_write) or (zi == 4 and self.can_write and self.can_read) or (zi == 3 and self.can_admin) ): free, total, zs = get_df(abspath, False) if total: if "vmaxb" in vn.flags: assert vn.lim # type: ignore # !rm total = vn.lim.vbmax if free == vn.lim.c_vb_r: free = min(free, max(0, vn.lim.vbmax - vn.lim.c_vb_v)) else: try: zi, _ = self.conn.hsrv.broker.ask( "up2k.get_volsizes", [vn.realpath] ).get()[0] vn.lim.c_vb_v = zi vn.lim.c_vb_r = free free = min(free, max(0, vn.lim.vbmax - zi)) except: pass h1 = humansize(free or 0) h2 = humansize(total) srv_info.append("{} free of {}".format(h1, h2)) elif zs: self.log("diskfree(%r): %s" % (abspath, zs), 3) srv_infot = " // ".join(srv_info) perms = [] if self.can_read or is_dk: perms.append("read") if self.can_write: perms.append("write") if self.can_move: perms.append("move") if self.can_delete: perms.append("delete") if self.can_get: perms.append("get") if self.can_upget: perms.append("upget") if self.can_admin: perms.append("admin") url_suf = self.urlq({}, ["k"]) is_ls = "ls" in self.uparam is_opds = "opds" in self.uparam is_js = self.args.force_js or self.cookies.get("js") == "y" if not is_ls and not add_og and self.ua.startswith(("curl/", "fetch")): self.uparam["ls"] = "v" is_ls = True tpl = "browser" if "b" in self.uparam: tpl = "browser2" is_js = False elif is_opds: # Display directory listing as OPDS v1.2 catalog feed if not (self.args.opds or "opds" in self.vn.flags): raise Pebkac(405, "OPDS is disabled in server config") if not self.can_read: raise Pebkac(401, "OPDS requires read permission") is_js = is_ls = False vf = vn.flags ls_ret = { "dirs": [], "files": [], "taglist": [], "srvinf": srv_infot, "acct": self.uname, "perms": perms, "cfg": vn.js_ls, } cgv = { "ls0": None, "acct": self.uname, "perms": perms, } # also see `js_htm` in authsrv.py j2a = { "cgv1": vn.js_htm, "cgv": cgv, "vpnodes": vpnodes, "files": [], "ls0": None, "taglist": [], "have_tags_idx": int(e2t), "have_b_u": (self.can_write and self.uparam.get("b") == "u"), "sb_lg": vn.js_ls["sb_lg"], "url_suf": url_suf, "title": html_escape("%s %s" % (self.args.bname, self.vpath), crlf=True), "srv_info": srv_infot, "dtheme": self.args.theme, } if self.args.js_browser: zs = self.args.js_browser zs += "&" if "?" in zs else "?" j2a["js"] = zs if self.args.css_browser: zs = self.args.css_browser zs += "&" if "?" in zs else "?" j2a["css"] = zs if not self.conn.hsrv.prism: j2a["no_prism"] = True if not self.can_read and not is_dk: logues, readmes = self._add_logues(vn, abspath, None) ls_ret["logues"] = j2a["logues"] = logues ls_ret["readmes"] = cgv["readmes"] = readmes if is_ls: return self.tx_ls(ls_ret) if not stat.S_ISDIR(st.st_mode): return self.tx_404(True) if "zip" in self.uparam or "tar" in self.uparam: raise Pebkac(403) zsl = j2a["files"] = [] if is_js: j2a["ls0"] = cgv["ls0"] = { "dirs": zsl, "files": zsl, "taglist": zsl, } html = self.j2s(tpl, **j2a) self.reply(html.encode("utf-8", "replace")) return True for k in ["zip", "tar"]: v = self.uparam.get(k) if v is not None and (not add_og or not og_fn): if is_dk and "dks" not in vn.flags: t = "server config does not allow download-as-zip/tar; only dk is specified, need dks too" raise Pebkac(403, t) return self.tx_zip(k, v, self.vpath, vn, rem, []) fsroot, vfs_ls, vfs_virt = vn.ls( rem, self.uname, not self.args.no_scandir, PERMS_rwh, lstat="lt" in self.uparam, throw=True, ) stats = {k: v for k, v in vfs_ls} ls_names = [x[0] for x in vfs_ls] ls_names.extend(list(vfs_virt)) if add_og and og_fn and not self.can_read: ls_names = [og_fn] is_js = True # check for old versions of files, # [num-backups, most-recent, hist-path] hist: dict[str, tuple[int, float, str]] = {} try: if vf["md_hist"] != "s": raise Exception() histdir = os.path.join(fsroot, ".hist") ptn = RE_MDV for hfn in bos.listdir(histdir): m = ptn.match(hfn) if not m: continue fn = m.group(1) + m.group(3) n, ts, _ = hist.get(fn, (0, 0, "")) hist[fn] = (n + 1, max(ts, float(m.group(2))), hfn) except: pass lnames = {x.lower(): x for x in ls_names} # show dotfiles if permitted and requested if not self.can_dot or ( "dots" not in self.uparam and (is_ls or "dots" not in self.cookies) ): ls_names = exclude_dotfiles(ls_names) add_dk = vf.get("dk") add_fk = vf.get("fk") fk_alg = 2 if "fka" in vf else 1 if add_dk: if vf.get("dky"): add_dk = False else: zs = self.gen_fk(2, self.args.dk_salt, abspath, 0, 0)[:add_dk] ls_ret["dk"] = cgv["dk"] = zs no_zip = bool(self._can_zip(vf)) dirs = [] files = [] ptn_hr = RE_HR use_abs_url = ( not is_opds and not is_ls and not is_js and not self.trailing_slash and vpath ) for fn in ls_names: base = "" href = fn if use_abs_url: base = "/" + vpath + "/" href = base + fn if fn in vfs_virt: fspath = vfs_virt[fn].realpath else: fspath = fsroot + "/" + fn try: linf = stats.get(fn) or bos.lstat(fspath) inf = bos.stat(fspath) if stat.S_ISLNK(linf.st_mode) else linf except: self.log("broken symlink: %r" % (fspath,)) continue is_dir = stat.S_ISDIR(inf.st_mode) if is_dir: href += "/" if no_zip: margin = "DIR" elif add_dk: zs = absreal(fspath) margin = 'zip' % ( quotep(href), self.gen_fk(2, self.args.dk_salt, zs, 0, 0)[:add_dk], ) else: margin = 'zip' % ( quotep(href), ) elif fn in hist: margin = '#%s' % ( base, html_escape(hist[fn][2], quot=True, crlf=True), hist[fn][0], ) else: margin = "-" sz = inf.st_size zd = datetime.fromtimestamp(max(0, linf.st_mtime), UTC) dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( zd.year, zd.month, zd.day, zd.hour, zd.minute, zd.second, ) if is_dir: ext = "---" elif "." in fn: ext = ptn_hr.sub("@", fn.rsplit(".", 1)[1]) if len(ext) > 16: ext = ext[:16] else: ext = "%" if add_fk and not is_dir: href = "%s?k=%s" % ( quotep(href), self.gen_fk( fk_alg, self.args.fk_salt, fspath, sz, 0 if ANYWIN else inf.st_ino, )[:add_fk], ) elif add_dk and is_dir: href = "%s?k=%s" % ( quotep(href), self.gen_fk(2, self.args.dk_salt, fspath, 0, 0)[:add_dk], ) else: href = quotep(href) item = { "lead": margin, "href": href, "name": fn, "sz": sz, "ext": ext, "dt": dt, "ts": int(linf.st_mtime), } if is_dir: dirs.append(item) else: files.append(item) if is_dk and not vf.get("dks"): dirs = [] if ( self.cookies.get("idxh") == "y" and "ls" not in self.uparam and "v" not in self.uparam and not is_opds ): for item in files: if item["name"] in IDX_HTML: # do full resolve in case of shadowed file vp = vjoin(self.vpath.split("?")[0], item["name"]) vn, rem = self.asrv.vfs.get(vp, self.uname, True, False) ap = vn.canonical(rem) if not self.trailing_slash and bos.path.isfile(ap): return self.redirect( self.vpath + "/", flavor="redirecting to", use302=True ) return self.tx_file("oh_f", ap) # is no-cache if icur: mte = vn.flags.get("mte") or {} tagset: set[str] = set() rd = vrem if self.can_admin: up_q = "select substr(w,1,16), ip, at, un from up where rd=? and fn=?" up_m = ["w", "up_ip", ".up_at", "up_by"] else: up_q, up_m = vn.flags["ls_q_m"] mt_q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'" for fe in files: fn = fe["name"] erd_efn = (rd, fn) try: r = icur.execute(mt_q, erd_efn) except Exception as ex: if "database is locked" in str(ex): break try: erd_efn = s3enc(idx.mem_cur, rd, fn) r = icur.execute(mt_q, erd_efn) except: self.log("tag read error, %r / %r\n%s" % (rd, fn, min_ex())) break tags = {k: v for k, v in r} if up_q: try: up_v = icur.execute(up_q, erd_efn).fetchone() for zs1, zs2 in zip(up_m, up_v): if zs2: tags[zs1] = zs2 except: pass _ = [tagset.add(k) for k in tags] fe["tags"] = tags for fe in dirs: fe["tags"] = ODict() lmte = list(mte) if self.can_admin: lmte.extend(("w", "up_by", "up_ip", ".up_at")) if "nodirsz" not in vf: tagset.add(".files") vdir = "%s/" % (rd,) if rd else "" q = "select sz, nf from ds where rd=? limit 1" for fe in dirs: try: hit = icur.execute(q, (vdir + fe["name"],)).fetchone() (fe["sz"], fe["tags"][".files"]) = hit except: pass # 404 or mojibake taglist = [k for k in lmte if k in tagset] else: taglist = [] logues, readmes = self._add_logues(vn, abspath, lnames) ls_ret["logues"] = j2a["logues"] = logues ls_ret["readmes"] = cgv["readmes"] = readmes if ( not files and not dirs and not readmes[0] and not readmes[1] and not logues[0] and not logues[1] ): logues[1] = "this folder is empty" if "descript.ion" in lnames and os.path.isfile( os.path.join(abspath, lnames["descript.ion"]) ): rem = [] items = {x["name"].lower(): x for x in files + dirs} with open(os.path.join(abspath, lnames["descript.ion"]), "rb") as f: for bln in [x.strip() for x in f]: try: if bln.endswith(b"\x04\xc2"): # multiline comment; replace literal r"\n" with " // " bln = bln.replace(br"\\n", b" // ")[:-2] ln = bln.decode("utf-8", "replace") if ln.startswith('"'): fn, desc = ln.split('" ', 1) fn = fn[1:] else: fn, desc = ln.split(" ", 1) try: item = items[fn.lower().strip("/")] except: t = "
  • %s %s
  • " rem.append(t % (html_escape(fn), html_escape(desc))) continue try: item["tags"]["descript.ion"] = desc except: item["tags"] = {"descript.ion": desc} except: pass if "descript.ion" not in taglist: taglist.insert(0, "descript.ion") if rem and not logues[1]: t = "

    descript.ion

      \n" logues[1] = t + "\n".join(rem) + "
    " if is_ls: ls_ret["dirs"] = dirs ls_ret["files"] = files ls_ret["taglist"] = taglist return self.tx_ls(ls_ret) doc = self.uparam.get("doc") if self.can_read else None if doc: zp = self.args.ua_nodoc if zp and zp.search(self.ua): t = "this URL contains no valuable information for bots/crawlers" raise Pebkac(403, t) j2a["docname"] = doc doctxt = None dfn = lnames.get(doc.lower()) if dfn and dfn != doc: # found Foo but want FOO dfn = next((x for x in files if x["name"] == doc), None) if dfn: docpath = os.path.join(abspath, doc) sz = bos.path.getsize(docpath) if sz < 1024 * self.args.txt_max: doctxt = read_utf8(self.log, fsenc(docpath), False) if doc.lower().endswith(".md") and "exp" in vn.flags: doctxt = self._expand(doctxt, vn.flags.get("exp_md") or []) else: self.log("doc 2big: %r" % (doc,), 6) doctxt = "( size of textfile exceeds serverside limit )" # NOTE: browser.js expects this exact message else: self.log("doc 404: %r" % (doc,), 6) doctxt = "( textfile not found )" if doctxt is not None: j2a["doc"] = doctxt for d in dirs: d["name"] += "/" dirs.sort(key=itemgetter("name")) if is_opds: # OpenSearch Description format requires a full-qualified URL and a "Short Name" under 16 characters # which will be the longname truncated in the template. # Relevant specs: # https://specs.opds.io/opds-1.2#3-search # https://developer.mozilla.org/en-US/docs/Web/XML/Guides/OpenSearch if "osd" in self.uparam: j2a["longname"] = "%s %s" % (self.args.bname, self.vpath) j2a["search_url"] = self.args.SRS + vpath xml = self.j2s("opds_osd", **j2a) self.reply( xml.encode("utf-8"), mime="application/opensearchdescription+xml" ) return True if "q" in self.uparam: q = self.uparam["q"] idx = self.conn.get_u2idx() if not idx: raise Pebkac(500, "indexer not available") # generate a raw query similar to web interface for multiple words r = " and ".join(("name like *%s*" % (x,)) for x in q.split()) hits, _, _ = idx.search(self.uname, [self.vn], r, 1000) files = [] dirs = [] prefix = quotep(vpath + "/" if vpath else "") for h in hits: rp = h["rp"] if not rp.startswith(prefix): continue zd = datetime.fromtimestamp(h["ts"], UTC) dt = "%04d-%02d-%02d %02d:%02d:%02d" % ( zd.year, zd.month, zd.day, zd.hour, zd.minute, zd.second, ) item = { "href": self.args.SRS + rp, "name": unquotep(rp[len(prefix) :].split("?")[0]), "sz": h["sz"], "dt": dt, "ts": h["ts"], } files.append(item) # exclude files which don't match --opds-exts allowed_exts = vf.get("opds_exts") or self.args.opds_exts if allowed_exts: files = [ x for x in files if x["name"].rsplit(".", 1)[-1] in allowed_exts ] j2a["opds_osd"] = "%s%s?opds&osd" % (self.args.SRS, quotep(vpath)) for item in dirs: href = item["href"] href += ("&" if "?" in href else "?") + "opds" item["href"] = href item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),) for item in files: href = item["href"] href += ("&" if "?" in href else "?") + "dl" item["href"] = href item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"),) if "rmagic" in self.vn.flags: ap = "%s/%s" % (fsroot, item["name"]) item["mime"] = guess_mime(item["name"], ap) else: item["mime"] = guess_mime(item["name"]) # Make sure we can actually generate JPEG thumbnails if ( not self.args.th_no_jpg and self.thumbcli and "dthumb" not in dbv.flags and "dithumb" not in dbv.flags ): item["jpeg_thumb_href"] = href + "&th=jf" item["jpeg_thumb_href_hires"] = item["jpeg_thumb_href"] + "3" j2a["files"] = files j2a["dirs"] = dirs html = self.j2s("opds", **j2a) mime = "application/atom+xml;profile=opds-catalog" self.reply(html.encode("utf-8", "replace"), mime=mime) return True if is_js: j2a["ls0"] = cgv["ls0"] = { "dirs": dirs, "files": files, "taglist": taglist, } j2a["files"] = [] else: j2a["files"] = dirs + files j2a["taglist"] = taglist if add_og and "raw" not in self.uparam: j2a["this"] = self cgv["og_fn"] = og_fn if og_fn and vn.flags.get("og_tpl"): tpl = vn.flags["og_tpl"] if "EXT" in tpl: zs = og_fn.split(".")[-1].lower() tpl2 = tpl.replace("EXT", zs) if os.path.exists(tpl2): tpl = tpl2 with self.conn.hsrv.mutex: if tpl not in self.conn.hsrv.j2: tdir, tname = os.path.split(tpl) j2env = jinja2.Environment() j2env.loader = jinja2.FileSystemLoader(tdir) self.conn.hsrv.j2[tpl] = j2env.get_template(tname) thumb = "" is_pic = is_vid = is_au = False for fn in self.args.th_coversd: if fn in lnames: thumb = lnames[fn] break if og_fn: ext = og_fn.split(".")[-1].lower() if self.thumbcli and ext in self.thumbcli.thumbable: is_pic = ( ext in self.thumbcli.fmt_pil or ext in self.thumbcli.fmt_vips or ext in self.thumbcli.fmt_ffi ) is_vid = ext in self.thumbcli.fmt_ffv is_au = ext in self.thumbcli.fmt_ffa if not thumb or not is_au: thumb = og_fn file = next((x for x in files if x["name"] == og_fn), None) else: file = None url_base = "%s://%s/%s" % ( "https" if self.is_https else "http", self.host, self.args.RS + quotep(vpath), ) j2a["og_is_pic"] = is_pic j2a["og_is_vid"] = is_vid j2a["og_is_au"] = is_au if thumb: fmt = vn.flags.get("og_th", "j") th_base = ujoin(url_base, quotep(thumb)) query = "th=%s&cache" % (fmt,) if use_filekey: query += "&k=" + self.uparam["k"] query = ub64enc(query.encode("utf-8")).decode("ascii") # discord looks at file extension, not content-type... query += "/th.jpg" if "j" in fmt else "/th.webp" j2a["og_thumb"] = "%s/.uqe/%s" % (th_base, query) j2a["og_fn"] = og_fn j2a["og_file"] = file if og_fn: og_fn_q = quotep(og_fn) query = "raw" if use_filekey: query += "&k=" + self.uparam["k"] query = ub64enc(query.encode("utf-8")).decode("ascii") query += "/%s" % (og_fn_q,) j2a["og_url"] = ujoin(url_base, og_fn_q) j2a["og_raw"] = j2a["og_url"] + "/.uqe/" + query else: j2a["og_url"] = j2a["og_raw"] = url_base if not vn.flags.get("og_no_head"): ogh = {"twitter:card": "summary"} title = str(vn.flags.get("og_title") or "") if thumb: ogh["og:image"] = j2a["og_thumb"] zso = vn.flags.get("og_desc") or "" if zso != "-": ogh["og:description"] = str(zso) zs = vn.flags.get("og_site") or self.args.name if zs not in ("", "-"): ogh["og:site_name"] = zs try: assert file is not None # type: ignore # !rm zs1, zs2 = file["tags"]["res"].split("x") file["tags"][".resw"] = zs1 file["tags"][".resh"] = zs2 except: pass tagmap = {} if is_au: title = str(vn.flags.get("og_title_a") or "") ogh["og:type"] = "music.song" ogh["og:audio"] = j2a["og_raw"] tagmap = { "artist": "og:music:musician", "album": "og:music:album", ".dur": "og:music:duration", } elif is_vid: title = str(vn.flags.get("og_title_v") or "") ogh["og:type"] = "video.other" ogh["og:video"] = j2a["og_raw"] tagmap = { "title": "og:title", ".dur": "og:video:duration", ".resw": "og:video:width", ".resh": "og:video:height", } elif is_pic: title = str(vn.flags.get("og_title_i") or "") ogh["twitter:card"] = "summary_large_image" ogh["twitter:image"] = ogh["og:image"] = j2a["og_raw"] tagmap = { ".resw": "og:image:width", ".resh": "og:image:height", } try: assert file is not None # type: ignore # !rm for k, v in file["tags"].items(): zs = "{{ %s }}" % (k,) title = title.replace(zs, str(v)) except: pass title = re.sub(r"\{\{ [^}]+ \}\}", "", title) while title.startswith(" - "): title = title[3:] while title.endswith(" - "): title = title[:3] if vn.flags.get("og_s_title") or not title: title = str(vn.flags.get("og_title") or "") for tag, hname in tagmap.items(): try: assert file is not None # type: ignore # !rm v = file["tags"][tag] if not v: continue ogh[hname] = int(v) if tag == ".dur" else v except: pass ogh["og:title"] = title oghs = [ '\t' % (k, html_escape(str(v), True, True)) for k, v in ogh.items() ] zs = self.html_head + "\n%s\n" % ("\n".join(oghs),) self.html_head = zs.replace("\n\n", "\n") html = self.j2s(tpl, **j2a) self.reply(html.encode("utf-8", "replace")) return True ================================================ FILE: copyparty/httpconn.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse # typechk import os import re import socket import threading # typechk import time try: if os.environ.get("PRTY_NO_TLS"): raise Exception() HAVE_SSL = True import ssl except: HAVE_SSL = False from . import util as Util from .__init__ import TYPE_CHECKING, EnvParams from .authsrv import AuthSrv # typechk from .httpcli import HttpCli from .ico import Ico from .mtag import HAVE_FFMPEG from .th_cli import ThumbCli from .th_srv import HAVE_PIL, HAVE_VIPS from .u2idx import U2idx from .util import HMaccas, NetMap, shut_socket if True: # pylint: disable=using-constant-test from typing import Optional, Pattern, Union if TYPE_CHECKING: from .httpsrv import HttpSrv PTN_HTTP = re.compile(br"[A-Z]{3}[A-Z ]") class HttpConn(object): """ spawned by HttpSrv to handle an incoming client connection, creates an HttpCli for each request (Connection: Keep-Alive) """ def __init__( self, sck: socket.socket, addr: tuple[str, int], hsrv: "HttpSrv" ) -> None: self.s = sck self.sr: Optional[Util._Unrecv] = None self.cli: Optional[HttpCli] = None self.addr = addr self.hsrv = hsrv self.u2mutex: threading.Lock = hsrv.u2mutex # mypy404 self.args: argparse.Namespace = hsrv.args # mypy404 self.E: EnvParams = self.args.E self.asrv: AuthSrv = hsrv.asrv # mypy404 self.u2fh: Util.FHC = hsrv.u2fh # mypy404 self.pipes: Util.CachedDict = hsrv.pipes # mypy404 self.ipu_iu: Optional[dict[str, str]] = hsrv.ipu_iu self.ipu_nm: Optional[NetMap] = hsrv.ipu_nm self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm self.ipar_nm: Optional[NetMap] = hsrv.ipar_nm self.xff_nm: Optional[NetMap] = hsrv.xff_nm self.xff_lan: NetMap = hsrv.xff_lan # type: ignore self.iphash: HMaccas = hsrv.broker.iphash self.bans: dict[str, int] = hsrv.bans self.aclose: dict[str, int] = hsrv.aclose enth = (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb self.thumbcli: Optional[ThumbCli] = ThumbCli(hsrv) if enth else None # mypy404 self.ico: Ico = Ico(self.args) # mypy404 self.t0: float = time.time() # mypy404 self.freshen_pwd: float = 0.0 self.stopping = False self.nreq: int = -1 # mypy404 self.nbyte: int = 0 # mypy404 self.u2idx: Optional[U2idx] = None self.log_func: "Util.RootLogger" = hsrv.log # mypy404 self.log_src: str = "httpconn" # mypy404 self.lf_url: Optional[Pattern[str]] = ( re.compile(self.args.lf_url) if self.args.lf_url else None ) # mypy404 self.set_rproxy() def shutdown(self) -> None: self.stopping = True try: shut_socket(self.log, self.s, 1) except: pass def set_rproxy(self, ip: Optional[str] = None) -> str: if ip is None: color = 36 ip = self.addr[0] self.rproxy = None else: color = 34 self.rproxy = ip self.ip = ip self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26) return self.log_src def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func(self.log_src, msg, c) def get_u2idx(self) -> Optional[U2idx]: # grab from a pool of u2idx instances; # sqlite3 fully parallelizes under python threads # but avoid running out of FDs by creating too many if not self.u2idx: self.u2idx = self.hsrv.get_u2idx(str(self.addr)) return self.u2idx def _detect_https(self) -> bool: try: method = self.s.recv(4, socket.MSG_PEEK) except socket.timeout: return False except AttributeError: # jython does not support msg_peek; forget about https method = self.s.recv(4) self.sr = Util.Unrecv(self.s, self.log) self.sr.buf = method # jython used to do this, they stopped since it's broken # but reimplementing sendall is out of scope for now if not getattr(self.s, "sendall", None): self.s.sendall = self.s.send # type: ignore if len(method) != 4: err = "need at least 4 bytes in the first packet; got {}".format( len(method) ) if method: self.log(err) self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8")) return False return not method or not bool(PTN_HTTP.match(method)) def run(self) -> None: self.s.settimeout(10) self.sr = None if self.args.https_only: is_https = True elif self.args.http_only: is_https = False else: # raise Exception("asdf") is_https = self._detect_https() if is_https: if self.sr: self.log("TODO: cannot do https in jython", c="1;31") return self.log_src = self.log_src.replace("[36m", "[35m") try: assert ssl # type: ignore # !rm ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.load_cert_chain(self.args.cert) if self.args.ssl_ver: ctx.options &= ~self.args.ssl_flags_en ctx.options |= self.args.ssl_flags_de # print(repr(ctx.options)) if self.args.ssl_log: try: ctx.keylog_filename = self.args.ssl_log except: self.log("keylog failed; openssl or python too old") if self.args.ciphers: ctx.set_ciphers(self.args.ciphers) self.s = ctx.wrap_socket(self.s, server_side=True) msg = [ "\033[1;3%dm%s" % (c, s) for c, s in zip([0, 5, 0], self.s.cipher()) # type: ignore ] self.log(" ".join(msg) + "\033[0m") if self.args.ssl_dbg and hasattr(self.s, "shared_ciphers"): ciphers = self.s.shared_ciphers() assert ciphers # !rm overlap = [str(y[::-1]) for y in ciphers] self.log("TLS cipher overlap:" + "\n".join(overlap)) for k, v in [ ["compression", self.s.compression()], ["ALPN proto", self.s.selected_alpn_protocol()], ["NPN proto", self.s.selected_npn_protocol()], ]: self.log("TLS {}: {}".format(k, v or "nah")) except Exception as ex: em = str(ex) if "ALERT_CERTIFICATE_UNKNOWN" in em: # android-chrome keeps doing this pass else: self.log("handshake\033[0m " + em, c=5) return if not self.sr: self.sr = Util.Unrecv(self.s, self.log) while not self.stopping: self.nreq += 1 self.cli = HttpCli(self) if not self.cli.run(): return if self.sr.te == 1: self.log("closing socket (leftover TE)", "90") return if ( "content-length" in self.cli.headers and int(self.cli.headers["content-length"]) != self.sr.nb ): self.log("closing socket (CL mismatch)", "90") return # note: proxies reject PUT sans Content-Length; illegal for HTTP/1.1 self.sr.nb = self.sr.te = 0 if self.u2idx: self.hsrv.put_u2idx(str(self.addr), self.u2idx) self.u2idx = None if self.rproxy: self.set_rproxy() ================================================ FILE: copyparty/httpsrv.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import hashlib import math import os import re import socket import sys import threading import time import queue from .__init__ import ANYWIN, CORES, EXE, MACOS, PY2, TYPE_CHECKING, EnvParams, unicode try: MNFE = ModuleNotFoundError except: MNFE = ImportError try: import jinja2 except MNFE: if EXE: raise print( """\033[1;31m you do not have jinja2 installed,\033[33m choose one of these:\033[0m * apt install python-jinja2 * {} -m pip install --user jinja2 * (try another python version, if you have one) * (try copyparty.sfx instead) """.format( sys.executable ) ) sys.exit(1) except SyntaxError: if EXE: raise print( """\033[1;31m your jinja2 version is incompatible with your python version;\033[33m please try to replace it with an older version:\033[0m * {} -m pip install --user jinja2==2.11.3 * (try another python version, if you have one) * (try copyparty.sfx instead) """.format( sys.executable ) ) sys.exit(1) from .httpconn import HttpConn from .metrics import Metrics from .u2idx import U2idx from .util import ( E_SCK, FHC, CachedDict, Daemon, Garda, Magician, Netdev, NetMap, build_netmap, has_resource, ipnorm, load_ipr, load_ipu, load_resource, min_ex, shut_socket, spack, start_log_thrs, start_stackmon, ub64enc, ) if TYPE_CHECKING: from .authsrv import VFS from .broker_util import BrokerCli from .ssdp import SSDPr if True: # pylint: disable=using-constant-test from typing import Any, Optional if PY2: range = xrange # type: ignore if not hasattr(socket, "AF_UNIX"): setattr(socket, "AF_UNIX", -9001) def load_jinja2_resource(E: EnvParams, name: str): with load_resource(E, "web/" + name, "r") as f: return f.read() class HttpSrv(object): """ handles incoming connections using HttpConn to process http, relying on MpSrv for performance (HttpSrv is just plain threads) """ def __init__(self, broker: "BrokerCli", nid: Optional[int]) -> None: self.broker = broker self.nid = nid self.args = broker.args self.E: EnvParams = self.args.E self.log = broker.log self.asrv = broker.asrv # redefine in case of multiprocessing socket.setdefaulttimeout(120) self.t0 = time.time() nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else "" self.magician = Magician() self.nm = NetMap([], []) self.ssdp: Optional["SSDPr"] = None self.gpwd = Garda(self.args.ban_pw) self.gpwc = Garda(self.args.ban_pwc) self.g404 = Garda(self.args.ban_404) self.g403 = Garda(self.args.ban_403) self.g422 = Garda(self.args.ban_422, False) self.gmal = Garda(self.args.ban_422) self.gurl = Garda(self.args.ban_url) self.bans: dict[str, int] = {} self.aclose: dict[str, int] = {} dli: dict[str, tuple[float, int, "VFS", str, str]] = {} # info dls: dict[str, tuple[float, int]] = {} # state self.dli = self.tdli = dli self.dls = self.tdls = dls self.iiam = '' % (self.args.SRS,) self.bound: set[tuple[str, int]] = set() self.name = "hsrv" + nsuf self.mutex = threading.Lock() self.u2mutex = threading.Lock() self.bad_ver = False self.stopping = False self.tp_nthr = 0 # actual self.tp_ncli = 0 # fading self.tp_time = 0.0 # latest worker collect self.tp_q: Optional[queue.LifoQueue[Any]] = ( None if self.args.no_htp else queue.LifoQueue() ) self.t_periodic: Optional[threading.Thread] = None self.u2fh = FHC() self.u2sc: dict[str, tuple[int, "hashlib._Hash"]] = {} self.pipes = CachedDict(0.2) self.metrics = Metrics(self) self.nreq = 0 self.nsus = 0 self.nban = 0 self.srvs: list[socket.socket] = [] self.ncli = 0 # exact self.clients: set[HttpConn] = set() # laggy self.nclimax = 0 self.cb_ts = 0.0 self.cb_v = "" self.u2idx_free: dict[str, U2idx] = {} self.u2idx_n = 0 assert jinja2 # type: ignore # !rm env = jinja2.Environment() env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f)) jn = [ "browser", "browser2", "cf", "idp", "md", "mde", "msg", "rups", "shares", "splash", "svcs", ] self.j2 = {x: env.get_template(x + ".html") for x in jn} self.j2["opds"] = env.get_template("opds.xml") self.j2["opds_osd"] = env.get_template("opds_osd.xml") self.prism = has_resource(self.E, "web/deps/prism.js.gz") if self.args.ipu: self.ipu_iu, self.ipu_nm = load_ipu(self.log, self.args.ipu) else: self.ipu_iu = self.ipu_nm = None if self.args.ipr: self.ipr = load_ipr(self.log, self.args.ipr) else: self.ipr = None self.ipa_nm = build_netmap(self.args.ipa) self.ipar_nm = build_netmap(self.args.ipar) self.xff_nm = build_netmap(self.args.xff_src) self.xff_lan = build_netmap("lan") self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split() if not self.args.no_dav: zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE" self.mallow += zs.split() if self.args.zs: from .ssdp import SSDPr self.ssdp = SSDPr(broker) if self.tp_q: self.start_threads(4) if nid: self.tdli = {} self.tdls = {} if self.args.stackmon: start_stackmon(self.args.stackmon, nid) if self.args.log_thrs: start_log_thrs(self.log, self.args.log_thrs, nid) self.th_cfg: dict[str, set[str]] = {} Daemon(self.post_init, "hsrv-init2") def post_init(self) -> None: try: x = self.broker.ask("thumbsrv.getcfg") self.th_cfg = x.get() except: pass def set_bad_ver(self) -> None: self.bad_ver = True def set_netdevs(self, netdevs: dict[str, Netdev]) -> None: ips = set() for ip, _ in self.bound: ips.add(ip) self.nm = NetMap(list(ips), list(netdevs)) def start_threads(self, n: int) -> None: self.tp_nthr += n if self.args.log_htp: self.log(self.name, "workers += {} = {}".format(n, self.tp_nthr), 6) for _ in range(n): Daemon(self.thr_poolw, self.name + "-poolw") def stop_threads(self, n: int) -> None: self.tp_nthr -= n if self.args.log_htp: self.log(self.name, "workers -= {} = {}".format(n, self.tp_nthr), 6) assert self.tp_q # !rm for _ in range(n): self.tp_q.put(None) def periodic(self) -> None: while True: time.sleep(2 if self.tp_ncli or self.ncli else 10) with self.u2mutex, self.mutex: self.u2fh.clean() if self.tp_q: self.tp_ncli = max(self.ncli, self.tp_ncli - 2) if self.tp_nthr > self.tp_ncli + 8: self.stop_threads(4) if not self.ncli and not self.u2fh.cache and self.tp_nthr <= 8: self.t_periodic = None return def listen(self, sck: socket.socket, nlisteners: int) -> None: tcp = sck.family != socket.AF_UNIX if self.args.j != 1: # lost in the pickle; redefine if not ANYWIN or self.args.reuseaddr: sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if tcp: sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sck.settimeout(None) # < does not inherit, ^ opts above do if tcp: ip, port = sck.getsockname()[:2] else: ip = re.sub(r"\.[0-9]+$", "", sck.getsockname().split("/")[-1]) port = 0 self.srvs.append(sck) self.bound.add((ip, port)) self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners) Daemon( self.thr_listen, "httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port), (sck,), ) def thr_listen(self, srv_sck: socket.socket) -> None: """listens on a shared tcp server""" fno = srv_sck.fileno() if srv_sck.family == socket.AF_UNIX: ip = re.sub(r"\.[0-9]+$", "", srv_sck.getsockname()) msg = "subscribed @ %s f%d p%d" % (ip, fno, os.getpid()) ip = ip.split("/")[-1] port = 0 tcp = False else: tcp = True ip, port = srv_sck.getsockname()[:2] hip = "[%s]" % (ip,) if ":" in ip else ip msg = "subscribed @ %s:%d f%d p%d" % (hip, port, fno, os.getpid()) self.log(self.name, msg) Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",)) saddr = ("", 0) # fwd-decl for `except TypeError as ex:` while not self.stopping: if self.args.log_conn: self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="90") spins = 0 while self.ncli >= self.nclimax: if not spins: t = "at connection limit (global-option 'nc'); waiting" self.log(self.name, t, 3) spins += 1 time.sleep(0.1) if spins != 50 or not self.args.aclose: continue ipfreq: dict[str, int] = {} with self.mutex: for c in self.clients: ip = ipnorm(c.ip) try: ipfreq[ip] += 1 except: ipfreq[ip] = 1 ip, n = sorted(ipfreq.items(), key=lambda x: x[1], reverse=True)[0] if n < self.nclimax / 2: continue self.aclose[ip] = int(time.time() + self.args.aclose * 60) nclose = 0 nloris = 0 nconn = 0 with self.mutex: for c in self.clients: cip = ipnorm(c.ip) if ip != cip: continue nconn += 1 try: if ( c.nreq >= 1 or not c.cli or c.cli.in_hdr_recv or c.cli.keepalive ): Daemon(c.shutdown) nclose += 1 if c.nreq <= 0 and (not c.cli or c.cli.in_hdr_recv): nloris += 1 except: pass t = "{} downgraded to connection:close for {} min; dropped {}/{} connections" self.log(self.name, t.format(ip, self.args.aclose, nclose, nconn), 1) if nloris < nconn / 2: continue t = "slow%s (idle-conn): %s banned for %d min" # slowloris self.log(self.name, t % ("loris", ip, self.args.loris), 1) self.bans[ip] = int(time.time() + self.args.loris * 60) if self.args.log_conn: self.log(self.name, "|%sC-acc1" % ("-" * 2,), c="90") try: sck, saddr = srv_sck.accept() if tcp: cip = unicode(saddr[0]) if cip.startswith("::ffff:"): cip = cip[7:] addr = (cip, saddr[1]) else: addr = ("127.8.3.7", sck.fileno()) except (OSError, socket.error) as ex: if self.stopping: break self.log(self.name, "accept({}): {}".format(fno, ex), c=6) time.sleep(0.02) continue except TypeError as ex: # on macOS, accept() may return a None saddr if blocked by LittleSnitch; # unicode(saddr[0]) ==> TypeError: 'NoneType' object is not subscriptable if tcp and not saddr: t = "accept(%s): failed to accept connection from client due to firewall or network issue" self.log(self.name, t % (fno,), c=3) try: sck.close() # type: ignore except: pass time.sleep(0.02) continue raise if self.args.log_conn: t = "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format( "-" * 3, ip, port % 8, port ) self.log("%s %s" % addr, t, c="90") self.accept(sck, addr) def accept(self, sck: socket.socket, addr: tuple[str, int]) -> None: """takes an incoming tcp connection and creates a thread to handle it""" now = time.time() if now - (self.tp_time or now) > 300: t = "httpserver threadpool died: tpt {:.2f}, now {:.2f}, nthr {}, ncli {}" self.log(self.name, t.format(self.tp_time, now, self.tp_nthr, self.ncli), 1) self.tp_time = 0 self.tp_q = None with self.mutex: self.ncli += 1 if not self.t_periodic: name = "hsrv-pt" if self.nid: name += "-%d" % (self.nid,) self.t_periodic = Daemon(self.periodic, name) if self.tp_q: self.tp_time = self.tp_time or now self.tp_ncli = max(self.tp_ncli, self.ncli) if self.tp_nthr < self.ncli + 4: self.start_threads(8) self.tp_q.put((sck, addr)) return if not self.args.no_htp: t = "looks like the httpserver threadpool died; please make an issue on github and tell me the story of how you pulled that off, thanks and dog bless\n" self.log(self.name, t, 1) Daemon( self.thr_client, "httpconn-%s-%d" % (addr[0].split(".", 2)[-1][-6:], addr[1]), (sck, addr), ) def thr_poolw(self) -> None: assert self.tp_q # !rm while True: task = self.tp_q.get() if not task: break with self.mutex: self.tp_time = 0 try: sck, addr = task me = threading.current_thread() me.name = "httpconn-%s-%d" % (addr[0].split(".", 2)[-1][-6:], addr[1]) self.thr_client(sck, addr) me.name = self.name + "-poolw" except Exception as ex: if str(ex).startswith("client d/c "): self.log(self.name, "thr_client: " + str(ex), 6) else: self.log(self.name, "thr_client: " + min_ex(), 3) def shutdown(self) -> None: self.stopping = True for srv in self.srvs: try: srv.close() except: pass thrs = [] clients = list(self.clients) for cli in clients: t = threading.Thread(target=cli.shutdown) thrs.append(t) t.start() if self.tp_q: self.stop_threads(self.tp_nthr) for _ in range(10): time.sleep(0.05) if self.tp_q.empty(): break for t in thrs: t.join() self.log(self.name, "ok bye") def thr_client(self, sck: socket.socket, addr: tuple[str, int]) -> None: """thread managing one tcp client""" cli = HttpConn(sck, addr, self) with self.mutex: self.clients.add(cli) # print("{}\n".format(len(self.clients)), end="") fno = sck.fileno() try: if self.args.log_conn: self.log("%s %s" % addr, "|%sC-crun" % ("-" * 4,), c="90") cli.run() except (OSError, socket.error) as ex: if ex.errno not in E_SCK: self.log( "%s %s" % addr, "run({}): {}".format(fno, ex), c=6, ) finally: sck = cli.s if self.args.log_conn: self.log("%s %s" % addr, "|%sC-cdone" % ("-" * 5,), c="90") try: fno = sck.fileno() shut_socket(cli.log, sck) except (OSError, socket.error) as ex: if not MACOS: self.log( "%s %s" % addr, "shut({}): {}".format(fno, ex), c="90", ) if ex.errno not in E_SCK: raise finally: with self.mutex: self.clients.remove(cli) self.ncli -= 1 if cli.u2idx: self.put_u2idx(str(addr), cli.u2idx) def cachebuster(self) -> str: if time.time() - self.cb_ts < 1: return self.cb_v with self.mutex: if time.time() - self.cb_ts < 1: return self.cb_v v = self.E.t0 try: with os.scandir(self.E.mod_ + "web") as dh: for fh in dh: inf = fh.stat() v = max(v, inf.st_mtime) except: pass # spack gives 4 lsb, take 3 lsb, get 4 ch self.cb_v = ub64enc(spack(b">L", int(v))[1:]).decode("ascii") self.cb_ts = time.time() return self.cb_v def get_u2idx(self, ident: str) -> Optional[U2idx]: utab = self.u2idx_free for _ in range(100): # 5/0.05 = 5sec with self.mutex: if utab: if ident in utab: return utab.pop(ident) return utab.pop(list(utab.keys())[0]) if self.u2idx_n < CORES: self.u2idx_n += 1 return U2idx(self) time.sleep(0.05) # not using conditional waits, on a hunch that # average performance will be faster like this # since most servers won't be fully saturated return None def put_u2idx(self, ident: str, u2idx: U2idx) -> None: with self.mutex: while ident in self.u2idx_free: ident += "a" self.u2idx_free[ident] = u2idx def read_dls( self, ) -> tuple[ dict[str, tuple[float, int, str, str, str]], dict[str, tuple[float, int]] ]: """ mp-broker asking for local dl-info + dl-state; reduce overhead by sending just the vfs vpath """ dli = {k: (a, b, c.vpath, d, e) for k, (a, b, c, d, e) in self.dli.items()} return (dli, self.dls) def write_dls( self, sdli: dict[str, tuple[float, int, str, str, str]], dls: dict[str, tuple[float, int]], ) -> None: """ mp-broker pushing total dl-info + dl-state; swap out the vfs vpath with the vfs node """ dli: dict[str, tuple[float, int, "VFS", str, str]] = {} for k, (a, b, c, d, e) in sdli.items(): vn = self.asrv.vfs.all_nodes[c] dli[k] = (a, b, vn, d, e) self.tdli = dli self.tdls = dls ================================================ FILE: copyparty/ico.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse # typechk import colorsys import hashlib import re from .__init__ import PY2 from .th_srv import HAVE_PIL, HAVE_PILF from .util import BytesIO, html_escape # type: ignore class Ico(object): def __init__(self, args: argparse.Namespace) -> None: self.args = args def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]: """placeholder to make thumbnails not break""" bext = ext.encode("ascii", "replace") ext = bext.decode("utf-8") zb = hashlib.sha1(bext).digest()[2:4] if PY2: zb = [ord(x) for x in zb] # type: ignore c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3) c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 0.8 if HAVE_PILF else 1, 1) ci = [int(x * 255) for x in list(c1) + list(c2)] c = "".join(["%02x" % (x,) for x in ci]) w = 100 h = 30 if as_thumb: sw, sh = self.args.th_size.split("x") h = int(100.0 / (float(sw) / float(sh))) if chrome: # cannot handle more than ~2000 unique SVGs if HAVE_PILF: # pillow 10.1 made this the default font; # svg: 3.7s, this: 36s try: from PIL import Image, ImageDraw # [.lt] are hard to see lowercase / unspaced ext2 = re.sub("(.)", "\\1 ", ext).upper() h = int(128.0 * h / w) w = 128 img = Image.new("RGB", (w, h), "#" + c[:6]) pb = ImageDraw.Draw(img) _, _, tw, th = pb.textbbox((0, 0), ext2, font_size=16) xy = (int((w - tw) / 2), int((h - th) / 2)) pb.text(xy, ext2, fill="#" + c[6:], font_size=16) img = img.resize((w * 2, h * 2), Image.NEAREST) buf = BytesIO() img.save(buf, format="PNG", compress_level=1) return "image/png", buf.getvalue() except: pass if HAVE_PIL: # svg: 3s, cache: 6s, this: 8s from PIL import Image, ImageDraw h = int(64.0 * h / w) w = 64 img = Image.new("RGB", (w, h), "#" + c[:6]) pb = ImageDraw.Draw(img) try: _, _, tw, th = pb.textbbox((0, 0), ext) except: tw, th = pb.textsize(ext) # type: ignore tw += len(ext) cw = tw // len(ext) x = ((w - tw) // 2) - (cw * 2) // 3 fill = "#" + c[6:] for ch in ext: pb.text((x, (h - th) // 2), " %s " % (ch,), fill=fill) x += cw img = img.resize((w * 3, h * 3), Image.NEAREST) buf = BytesIO() img.save(buf, format="PNG", compress_level=1) return "image/png", buf.getvalue() svg = """\ {} """ txt = html_escape(ext, True) if "\n" in txt: lines = txt.split("\n") n = len(lines) y = "20%" if n == 2 else "10%" if n == 3 else "0" zs = '%s' txt = "".join([zs % (x,) for x in lines]) else: y = "50%" svg = svg.format(h, c[:6], y, c[6:], txt) return "image/svg+xml", svg.encode("utf-8") ================================================ FILE: copyparty/mdns.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import errno import os import random import select import socket import time from ipaddress import IPv4Network, IPv6Network from .__init__ import TYPE_CHECKING from .__init__ import unicode as U from .multicast import MC_Sck, MCast from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex try: if os.environ.get("PRTY_SYS_ALL") or os.environ.get("PRTY_SYS_DNSLIB"): raise ImportError() from .stolen.dnslib import ( AAAA, ) from .stolen.dnslib import CLASS as DC from .stolen.dnslib import ( NSEC, PTR, QTYPE, RR, SRV, TXT, A, DNSHeader, DNSQuestion, DNSRecord, set_avahi_379, ) DNS_VND = True except ImportError: DNS_VND = False from dnslib import ( AAAA, ) from dnslib import CLASS as DC from dnslib import ( NSEC, PTR, QTYPE, RR, SRV, TXT, A, Bimap, DNSHeader, DNSQuestion, DNSRecord, ) DC.forward[0x8001] = "F_IN" DC.reverse["F_IN"] = 0x8001 if TYPE_CHECKING: from .svchub import SvcHub if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union if os.environ.get("PRTY_MODSPEC"): from inspect import getsourcefile print("PRTY_MODSPEC: dnslib:", getsourcefile(A)) MDNS4 = "224.0.0.251" MDNS6 = "ff02::fb" class MDNS_Sck(MC_Sck): def __init__( self, sck: socket.socket, nd: Netdev, grp: str, ip: str, net: Union[IPv4Network, IPv6Network], ): super(MDNS_Sck, self).__init__(sck, nd, grp, ip, net) self.bp_probe = b"" self.bp_ip = b"" self.bp_svc = b"" self.bp_bye = b"" self.last_tx = 0.0 self.tx_ex = False class MDNS(MCast): def __init__(self, hub: "SvcHub", ngen: int) -> None: al = hub.args grp4 = "" if al.zm6 else MDNS4 grp6 = "" if al.zm4 else MDNS6 super(MDNS, self).__init__( hub, MDNS_Sck, al.zm_on, al.zm_off, grp4, grp6, 5353, hub.args.zmv ) self.srv: dict[socket.socket, MDNS_Sck] = {} self.logsrc = "mDNS-{}".format(ngen) self.ngen = ngen self.ttl = 300 if not self.args.zm_nwa_1 and DNS_VND: set_avahi_379() # type: ignore zs = self.args.zm_fqdn or (self.args.name + ".local") zs = zs.replace("--name", self.args.name).rstrip(".") + "." zs = zs.encode("ascii", "replace").decode("ascii", "replace") self.hn = "-".join(x for x in zs.split("?") if x) or ( "vault-{}".format(random.randint(1, 255)) ) self.lhn = self.hn.lower() # requester ip -> (response deadline, srv, body): self.q: dict[str, tuple[float, MDNS_Sck, bytes]] = {} self.rx4 = CachedSet(0.42) # 3 probes @ 250..500..750 => 500ms span self.rx6 = CachedSet(0.42) self.svcs, self.sfqdns = self.build_svcs() self.lsvcs = {k.lower(): v for k, v in self.svcs.items()} self.lsfqdns = set([x.lower() for x in self.sfqdns]) self.probing = 0.0 self.unsolicited: list[float] = [] # scheduled announces on all nics self.defend: dict[MDNS_Sck, float] = {} # server -> deadline def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func(self.logsrc, msg, c) def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]: ar = self.args zms = self.args.zms zi = ar.zm_http http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]} zi = ar.zm_https https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]} webdav = http.copy() webdavs = https.copy() webdav["u"] = webdavs["u"] = "u" # KDE requires username ftp = {"port": (self.args.ftp if "f" in zms else self.args.ftps)} smb = {"port": self.args.smb_port} # some gvfs require path zs = self.args.zm_ld or "/" if zs: webdav["path"] = zs webdavs["path"] = zs if self.args.zm_lh: http["path"] = self.args.zm_lh https["path"] = self.args.zm_lh if self.args.zm_lf: ftp["path"] = self.args.zm_lf if self.args.zm_ls: smb["path"] = self.args.zm_ls svcs: dict[str, dict[str, Any]] = {} if "d" in zms and http["port"]: svcs["_webdav._tcp.local."] = webdav if "D" in zms and https["port"]: svcs["_webdavs._tcp.local."] = webdavs if "h" in zms and http["port"]: svcs["_http._tcp.local."] = http if "H" in zms and https["port"]: svcs["_https._tcp.local."] = https if "f" in zms.lower(): svcs["_ftp._tcp.local."] = ftp if "s" in zms.lower(): svcs["_smb._tcp.local."] = smb sfqdns: set[str] = set() for k, v in svcs.items(): name = "{}-c-{}".format(self.args.name, k.split(".")[0][1:]) v["name"] = name sfqdns.add("{}.{}".format(name, k)) return svcs, sfqdns def build_replies(self) -> None: for srv in self.srv.values(): probe = DNSRecord(DNSHeader(0, 0), q=DNSQuestion(self.hn, QTYPE.ANY)) areply = DNSRecord(DNSHeader(0, 0x8400)) sreply = DNSRecord(DNSHeader(0, 0x8400)) bye = DNSRecord(DNSHeader(0, 0x8400)) have4 = have6 = False for s2 in self.srv.values(): if srv.idx != s2.idx: continue if s2.v6: have6 = True else: have4 = True for ip in srv.ips: if ":" in ip: qt = QTYPE.AAAA ar = {"rclass": DC.F_IN, "rdata": AAAA(ip)} else: qt = QTYPE.A ar = {"rclass": DC.F_IN, "rdata": A(ip)} r0 = RR(self.hn, qt, ttl=0, **ar) r120 = RR(self.hn, qt, ttl=120, **ar) # rfc-10: # SHOULD rr ttl 120sec for A/AAAA/SRV # (and recommend 75min for all others) probe.add_auth(r120) areply.add_answer(r120) sreply.add_answer(r120) bye.add_answer(r0) for sclass, props in self.svcs.items(): sname = props["name"] sport = props["port"] sfqdn = sname + "." + sclass k = "_services._dns-sd._udp.local." r = RR(k, QTYPE.PTR, DC.IN, 4500, PTR(sclass)) sreply.add_answer(r) r = RR(sclass, QTYPE.PTR, DC.IN, 4500, PTR(sfqdn)) sreply.add_answer(r) r = RR(sfqdn, QTYPE.SRV, DC.F_IN, 120, SRV(0, 0, sport, self.hn)) sreply.add_answer(r) areply.add_answer(r) r = RR(sfqdn, QTYPE.SRV, DC.F_IN, 0, SRV(0, 0, sport, self.hn)) bye.add_answer(r) txts = [] for k in ("u", "path"): if k not in props: continue zb = "{}={}".format(k, props[k]).encode("utf-8") if len(zb) > 255: t = "value too long for mdns: [{}]" raise Exception(t.format(props[k])) txts.append(zb) # gvfs really wants txt even if they're empty r = RR(sfqdn, QTYPE.TXT, DC.F_IN, 4500, TXT(txts)) sreply.add_answer(r) if not (have4 and have6) and not self.args.zm_noneg: ns = NSEC(self.hn, ["AAAA" if have6 else "A"]) r = RR(self.hn, QTYPE.NSEC, DC.F_IN, 120, ns) areply.add_ar(r) if len(sreply.pack()) < 1400: sreply.add_ar(r) srv.bp_probe = probe.pack() srv.bp_ip = areply.pack() srv.bp_svc = sreply.pack() srv.bp_bye = bye.pack() # since all replies are small enough to fit in one packet, # always send full replies rather than just a/aaaa records srv.bp_ip = srv.bp_svc def send_probes(self) -> None: slp = random.random() * 0.25 for _ in range(3): time.sleep(slp) slp = 0.25 if not self.running: break if self.args.zmv: self.log("sending hostname probe...") # ipv4: need to probe each ip (each server) # ipv6: only need to probe each set of looped nics probed6: set[str] = set() for srv in self.srv.values(): if srv.ip in probed6: continue try: srv.sck.sendto(srv.bp_probe, (srv.grp, 5353)) if srv.v6: for ip in srv.ips: probed6.add(ip) except Exception as ex: self.log("sendto failed: {} ({})".format(srv.ip, ex), "90") def run(self) -> None: try: bound = self.create_servers() except: t = "no server IP matches the mdns config\n{}" self.log(t.format(min_ex()), 1) bound = [] if not bound: self.log("failed to announce copyparty services on the network", 3) return self.build_replies() Daemon(self.send_probes) zf = time.time() + 2 self.probing = zf # cant unicast so give everyone an extra sec self.unsolicited = [zf, zf + 1, zf + 3, zf + 7] # rfc-8.3 try: self.run2() except OSError as ex: if ex.errno != errno.EBADF: raise self.log("stopping due to {}".format(ex), "90") self.log("stopped", 2) def run2(self) -> None: last_hop = time.time() ihop = self.args.mc_hop try: if self.args.no_poll: raise Exception() fd2sck = {} srvpoll = select.poll() for sck in self.srv: fd = sck.fileno() fd2sck[fd] = sck srvpoll.register(fd, select.POLLIN) except Exception as ex: srvpoll = None if not self.args.no_poll: t = "WARNING: failed to poll(), will use select() instead: %r" self.log(t % (ex,), 3) while self.running: timeout = ( 0.02 + random.random() * 0.07 if self.probing or self.q or self.defend else max(0.05, self.unsolicited[0] - time.time()) if self.unsolicited else (last_hop + ihop if ihop else 180) ) if srvpoll: pr = srvpoll.poll(timeout * 1000) rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN] else: rdy = select.select(self.srv, [], [], timeout) rx: list[socket.socket] = rdy[0] # type: ignore self.rx4.cln() self.rx6.cln() buf = b"" addr = ("0", 0) for sck in rx: try: buf, addr = sck.recvfrom(4096) self.eat(buf, addr, sck) except: if not self.running: self.log("stopped", 2) return if self.args.zm_no_pe: continue t = "{} {} \033[33m|{}| {}\n{}".format( self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex() ) self.log(t, 6) if not self.probing: self.process() continue if self.probing < time.time(): t = "probe ok; announcing [{}]" self.log(t.format(self.hn[:-1]), 2) self.probing = 0 def stop(self, panic=False) -> None: self.running = False for srv in self.srv.values(): try: if panic: srv.sck.close() else: srv.sck.sendto(srv.bp_bye, (srv.grp, 5353)) except: pass self.srv.clear() def eat(self, buf: bytes, addr: tuple[str, int], sck: socket.socket) -> None: cip = addr[0] v6 = ":" in cip if (cip.startswith("169.254") and not self.ll_ok) or ( v6 and not cip.startswith(IP6_LL) ): return cache = self.rx6 if v6 else self.rx4 if buf in cache.c: return srv: Optional[MDNS_Sck] = self.srv[sck] if v6 else self.map_client(cip) # type: ignore if not srv: return cache.add(buf) now = time.time() if self.args.zmv and cip != srv.ip and cip not in srv.ips: t = "{} [{}] \033[36m{} \033[0m|{}|" self.log(t.format(srv.name, srv.ip, cip, len(buf)), "90") p = DNSRecord.parse(buf) if self.args.zmvv: self.log(str(p)) # check for incoming probes for our hostname cips = [U(x.rdata) for x in p.auth if U(x.rname).lower() == self.lhn] if cips and self.sips.isdisjoint(cips): if not [x for x in cips if x not in ("::1", "127.0.0.1")]: # avahi broadcasting 127.0.0.1-only packets return self.log("someone trying to steal our hostname: {}".format(cips), 3) # immediately unicast if not self.probing: srv.sck.sendto(srv.bp_ip, (cip, 5353)) # and schedule multicast self.defend[srv] = self.defend.get(srv, now + 0.1) return # check for someone rejecting our probe / hijacking our hostname cips = [ U(x.rdata) for x in p.rr if U(x.rname).lower() == self.lhn and x.rclass == DC.F_IN ] if cips and self.sips.isdisjoint(cips): if not [x for x in cips if x not in ("::1", "127.0.0.1")]: # avahi broadcasting 127.0.0.1-only packets return # check if we've been given additional IPs for ip in list_ips(): if ip in cips: self.sips.add(ip) if not self.sips.isdisjoint(cips): return t = "mdns zeroconf: " if self.probing: t += "Cannot start; hostname '{}' is occupied" else: t += "Emergency stop; hostname '{}' got stolen" t += " on {}! Use --name to set another hostname.\n\nName taken by {}\n\nYour IPs: {}\n" self.log(t.format(self.args.name, srv.name, cips, list(self.sips)), 1) self.stop(True) return # then rfc-6.7; dns pretending to be mdns (android...) if p.header.id or addr[1] != 5353: rsp: Optional[DNSRecord] = None for r in p.questions: try: lhn = U(r.qname).lower() except: self.log("invalid question: {}".format(r)) continue if lhn != self.lhn: continue if p.header.id and r.qtype in (QTYPE.A, QTYPE.AAAA): rsp = rsp or DNSRecord(DNSHeader(p.header.id, 0x8400)) rsp.add_question(r) for ip in srv.ips: qt = r.qtype v6 = ":" in ip if v6 == (qt == QTYPE.AAAA): rd = AAAA(ip) if v6 else A(ip) rr = RR(self.hn, qt, DC.IN, 10, rd) rsp.add_answer(rr) if rsp: srv.sck.sendto(rsp.pack(), addr[:2]) # but don't return in case it's a differently broken client # then a/aaaa records for r in p.questions: try: lhn = U(r.qname).lower() except: self.log("invalid question: {}".format(r)) continue if lhn != self.lhn: continue # gvfs keeps repeating itself found = False unicast = False for rr in p.rr: try: rname = U(rr.rname).lower() except: self.log("invalid rr: {}".format(rr)) continue if rname == self.lhn: if rr.ttl > 60: found = True if rr.rclass == DC.F_IN: unicast = True if unicast: # spec-compliant mDNS-over-unicast srv.sck.sendto(srv.bp_ip, (cip, 5353)) elif addr[1] != 5353: # just in case some clients use (and want us to use) invalid ports srv.sck.sendto(srv.bp_ip, addr[:2]) if not found: self.q[cip] = (0, srv, srv.bp_ip) return deadline = now + (0.5 if p.header.tc else 0.02) # rfc-7.2 # and service queries for r in p.questions: if not r or not r.qname: continue qname = U(r.qname).lower() if qname in self.lsvcs or qname == "_services._dns-sd._udp.local.": self.q[cip] = (deadline, srv, srv.bp_svc) break # heed rfc-7.1 if there was an announce in the past 12sec # (workaround gvfs race-condition where it occasionally # doesn't read/decode the full response...) if now < srv.last_tx + 12: for rr in p.rr: if not rr.rdata: continue rdata = U(rr.rdata).lower() if rdata in self.lsfqdns: if rr.ttl > 2250: self.q.pop(cip, None) break def process(self) -> None: tx = set() now = time.time() cooldown = 0.9 # rfc-6: 1 if self.unsolicited and self.unsolicited[0] < now: self.unsolicited.pop(0) cooldown = 0.1 for srv in self.srv.values(): tx.add(srv) if not self.unsolicited and self.args.zm_spam: zf = time.time() + self.args.zm_spam + random.random() * 0.07 self.unsolicited.append(zf) for srv, deadline in list(self.defend.items()): if now < deadline: continue if self._tx(srv, srv.bp_ip, 0.02): # rfc-6: 0.25 self.defend.pop(srv) for cip, (deadline, srv, msg) in list(self.q.items()): if now < deadline: continue self.q.pop(cip) self._tx(srv, msg, cooldown) for srv in tx: self._tx(srv, srv.bp_svc, cooldown) def _tx(self, srv: MDNS_Sck, msg: bytes, cooldown: float) -> bool: now = time.time() if now < srv.last_tx + cooldown: return False try: srv.sck.sendto(msg, (srv.grp, 5353)) srv.last_tx = now except Exception as ex: if srv.tx_ex: return True srv.tx_ex = True t = "tx({},|{}|,{}): {}" self.log(t.format(srv.ip, len(msg), cooldown, ex), 3) return True ================================================ FILE: copyparty/metrics.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import json import time from .__init__ import TYPE_CHECKING from .util import Pebkac, get_df, unhumanize if TYPE_CHECKING: from .httpcli import HttpCli from .httpsrv import HttpSrv class Metrics(object): def __init__(self, hsrv: "HttpSrv") -> None: self.hsrv = hsrv def tx(self, cli: "HttpCli") -> bool: args = cli.args if not cli.avol and cli.uname.lower() not in args.stats_u_set: raise Pebkac(403, "'stats' not allowed for user " + cli.uname) if not args.stats: raise Pebkac(403, "the stats feature is not enabled in server config") conn = cli.conn vfs = conn.asrv.vfs allvols = list(sorted(vfs.all_vols.items())) idx = conn.get_u2idx() if not idx or not hasattr(idx, "p_end"): idx = None ret: list[str] = [] def addc(k: str, v: str, desc: str) -> None: zs = "# TYPE %s counter\n# HELP %s %s\n%s_created %s\n%s_total %s" ret.append(zs % (k, k, desc, k, int(self.hsrv.t0), k, v)) def adduc(k: str, unit: str, v: str, desc: str) -> None: k += "_" + unit zs = "# TYPE %s counter\n# UNIT %s %s\n# HELP %s %s\n%s_created %s\n%s_total %s" ret.append(zs % (k, k, unit, k, desc, k, int(self.hsrv.t0), k, v)) def addg(k: str, v: str, desc: str) -> None: zs = "# TYPE %s gauge\n# HELP %s %s\n%s %s" ret.append(zs % (k, k, desc, k, v)) def addug(k: str, unit: str, v: str, desc: str) -> None: k += "_" + unit zs = "# TYPE %s gauge\n# UNIT %s %s\n# HELP %s %s\n%s %s" ret.append(zs % (k, k, unit, k, desc, k, v)) def addh(k: str, typ: str, desc: str) -> None: zs = "# TYPE %s %s\n# HELP %s %s" ret.append(zs % (k, typ, k, desc)) def addbh(k: str, desc: str) -> None: zs = "# TYPE %s gauge\n# UNIT %s bytes\n# HELP %s %s" ret.append(zs % (k, k, k, desc)) def addv(k: str, v: str) -> None: ret.append("%s %s" % (k, v)) t = "time since last copyparty restart" v = "{:.3f}".format(time.time() - self.hsrv.t0) addug("cpp_uptime", "seconds", v, t) # timestamps are gauges because initial value is not zero t = "unixtime of last copyparty restart" v = "{:.3f}".format(self.hsrv.t0) addug("cpp_boot_unixtime", "seconds", v, t) t = "number of active downloads" addg("cpp_active_dl", str(len(self.hsrv.tdls)), t) t = "number of open http(s) client connections" addg("cpp_http_conns", str(self.hsrv.ncli), t) t = "number of http(s) requests since last restart" addc("cpp_http_reqs", str(self.hsrv.nreq), t) t = "number of 403/422/malicious reqs since restart" addc("cpp_sus_reqs", str(self.hsrv.nsus), t) v = str(len(conn.bans or [])) addg("cpp_active_bans", v, "number of currently banned IPs") t = "number of IPs banned since last restart" addg("cpp_total_bans", str(self.hsrv.nban), t) if not args.nos_vst: x = self.hsrv.broker.ask("up2k.get_state", True, "") vs = json.loads(x.get()) nvidle = 0 nvbusy = 0 nvoffline = 0 for v in vs["volstate"].values(): if v == "online, idle": nvidle += 1 elif "OFFLINE" in v: nvoffline += 1 else: nvbusy += 1 addg("cpp_idle_vols", str(nvidle), "number of idle/ready volumes") addg("cpp_busy_vols", str(nvbusy), "number of busy/indexing volumes") addg("cpp_offline_vols", str(nvoffline), "number of offline volumes") t = "time since last database activity (upload/rename/delete)" addug("cpp_db_idle", "seconds", str(vs["dbwt"]), t) t = "unixtime of last database activity (upload/rename/delete)" addug("cpp_db_act", "seconds", str(vs["dbwu"]), t) t = "number of files queued for hashing/indexing" addg("cpp_hashing_files", str(vs["hashq"]), t) t = "number of files queued for metadata scanning" addg("cpp_tagq_files", str(vs["tagq"]), t) try: t = "number of files queued for plugin-based analysis" addg("cpp_mtpq_files", str(int(vs["mtpq"])), t) except: pass if not args.nos_hdd: addbh("cpp_disk_size_bytes", "total HDD size of volume") addbh("cpp_disk_free_bytes", "free HDD space in volume") for vpath, vol in allvols: free, total, _ = get_df(vol.realpath, False) if free is None or total is None: continue addv('cpp_disk_size_bytes{vol="/%s"}' % (vpath), str(total)) addv('cpp_disk_free_bytes{vol="/%s"}' % (vpath), str(free)) if idx and not args.nos_vol: addbh("cpp_vol_bytes", "num bytes of data in volume") addh("cpp_vol_files", "gauge", "num files in volume") addbh("cpp_vol_free_bytes", "free space (vmaxb) in volume") addh("cpp_vol_free_files", "gauge", "free space (vmaxn) in volume") tnbytes = 0 tnfiles = 0 volsizes = [] try: ptops = [x.realpath for _, x in allvols] x = self.hsrv.broker.ask("up2k.get_volsizes", ptops) volsizes = x.get() except Exception as ex: cli.log("tx_stats get_volsizes: {!r}".format(ex), 3) for (vpath, vol), (nbytes, nfiles) in zip(allvols, volsizes): tnbytes += nbytes tnfiles += nfiles addv('cpp_vol_bytes{vol="/%s"}' % (vpath), str(nbytes)) addv('cpp_vol_files{vol="/%s"}' % (vpath), str(nfiles)) if vol.flags.get("vmaxb") or vol.flags.get("vmaxn"): zi = unhumanize(vol.flags.get("vmaxb") or "0") if zi: v = str(zi - nbytes) addv('cpp_vol_free_bytes{vol="/%s"}' % (vpath), v) zi = unhumanize(vol.flags.get("vmaxn") or "0") if zi: v = str(zi - nfiles) addv('cpp_vol_free_files{vol="/%s"}' % (vpath), v) if volsizes: addv('cpp_vol_bytes{vol="total"}', str(tnbytes)) addv('cpp_vol_files{vol="total"}', str(tnfiles)) if idx and not args.nos_dup: addbh("cpp_dupe_bytes", "num dupe bytes in volume") addh("cpp_dupe_files", "gauge", "num dupe files in volume") tnbytes = 0 tnfiles = 0 for vpath, vol in allvols: cur = idx.get_cur(vol) if not cur: continue nbytes = 0 nfiles = 0 q = "select sz, count(*)-1 c from up group by w having c" for sz, c in cur.execute(q): nbytes += sz * c nfiles += c tnbytes += nbytes tnfiles += nfiles addv('cpp_dupe_bytes{vol="/%s"}' % (vpath), str(nbytes)) addv('cpp_dupe_files{vol="/%s"}' % (vpath), str(nfiles)) addv('cpp_dupe_bytes{vol="total"}', str(tnbytes)) addv('cpp_dupe_files{vol="total"}', str(tnfiles)) if not args.nos_unf: addbh("cpp_unf_bytes", "incoming/unfinished uploads (num bytes)") addh("cpp_unf_files", "gauge", "incoming/unfinished uploads (num files)") tnbytes = 0 tnfiles = 0 try: x = self.hsrv.broker.ask("up2k.get_unfinished") xs = x.get() if not xs: raise Exception("up2k mutex acquisition timed out") xj = json.loads(xs) for ptop, (nbytes, nfiles) in xj.items(): tnbytes += nbytes tnfiles += nfiles vol = next((x[1] for x in allvols if x[1].realpath == ptop), None) if not vol: t = "tx_stats get_unfinished: could not map {}" cli.log(t.format(ptop), 3) continue addv('cpp_unf_bytes{vol="/%s"}' % (vol.vpath), str(nbytes)) addv('cpp_unf_files{vol="/%s"}' % (vol.vpath), str(nfiles)) addv('cpp_unf_bytes{vol="total"}', str(tnbytes)) addv('cpp_unf_files{vol="total"}', str(tnfiles)) except Exception as ex: cli.log("tx_stats get_unfinished: {!r}".format(ex), 3) ret.append("# EOF") mime = "application/openmetrics-text; version=1.0.0; charset=utf-8" mime = cli.uparam.get("mime") or mime cli.reply("\n".join(ret).encode("utf-8"), mime=mime) return True ================================================ FILE: copyparty/mtag.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import json import os import re import shutil import subprocess as sp import sys import tempfile from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode from .authsrv import VFS from .bos import bos from .util import ( FFMPEG_URL, REKOBO_LKEY, VF_CAREFUL, fsenc, gzip, min_ex, pybin, retchk, runcmd, sfsenc, uncyg, wunlink, ) if True: # pylint: disable=using-constant-test from typing import IO, Any, Optional, Union from .util import NamedLogger, RootLogger try: if os.environ.get("PRTY_NO_MUTAGEN"): raise Exception() from mutagen import version # noqa: F401 HAVE_MUTAGEN = True except: HAVE_MUTAGEN = False def have_ff(scmd: str) -> bool: if ANYWIN: scmd += ".exe" if PY2: print("# checking {}".format(scmd)) acmd = (scmd + " -version").encode("ascii").split(b" ") try: sp.Popen(acmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate() return True except: return False else: return bool(shutil.which(scmd)) HAVE_FFMPEG = not os.environ.get("PRTY_NO_FFMPEG") and have_ff("ffmpeg") HAVE_FFPROBE = not os.environ.get("PRTY_NO_FFPROBE") and have_ff("ffprobe") CBZ_PICS = set("png jpg jpeg gif bmp tga tif tiff webp avif jxl".split()) CBZ_01 = re.compile(r"(^|[^0-9v])0+[01]\b") FMT_AU = set("mp3 ogg flac wav".split()) M4A = set("aac m4a m4b m4r".split()) class MParser(object): def __init__(self, cmdline: str) -> None: self.tag, args = cmdline.split("=", 1) self.tags = self.tag.split(",") self.timeout = 60 self.force = False self.kill = "t" # tree; all children recursively self.capture = 3 # outputs to consume self.audio = "y" self.pri = 0 # priority; higher = later self.ext = [] while True: try: bp = os.path.expanduser(args) if WINDOWS: bp = uncyg(bp) if bos.path.exists(bp): self.bin = bp return except: pass arg, args = args.split(",", 1) arg = arg.lower() if arg.startswith("a"): self.audio = arg[1:] # [r]equire [n]ot [d]ontcare continue if arg.startswith("k"): self.kill = arg[1:] # [t]ree [m]ain [n]one continue if arg.startswith("c"): self.capture = int(arg[1:]) # 0=none 1=stdout 2=stderr 3=both continue if arg == "f": self.force = True continue if arg.startswith("t"): self.timeout = int(arg[1:]) continue if arg.startswith("e"): self.ext.append(arg[1:]) continue if arg.startswith("p"): self.pri = int(arg[1:] or "1") continue raise Exception() def au_unpk( log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None ) -> str: fd = 0 ret = "" maxsz = 1024 * 1024 * 64 try: ext = abspath.split(".")[-1].lower() au, pk = fmt_map[ext].split(".") fd, ret = tempfile.mkstemp("." + au) if pk == "gz": fi = gzip.GzipFile(abspath, mode="rb") elif pk == "xz": import lzma fi = lzma.open(abspath, "rb") elif pk == "zip": import zipfile zf = zipfile.ZipFile(abspath, "r") zil = zf.infolist() zil = [x for x in zil if x.filename.lower().split(".")[-1] == au] if not zil: raise Exception("no audio inside zip") fi = zf.open(zil[0]) elif pk == "cbz": import zipfile zf = zipfile.ZipFile(abspath, "r") znil = [(x.filename.lower(), x) for x in zf.infolist()] nf = len(znil) znil = [x for x in znil if x[0].split(".")[-1] in CBZ_PICS] znil = [x for x in znil if "cover" in x[0]] or znil znil = [x for x in znil if CBZ_01.search(x[0])] or znil t = "cbz: %d files, %d hits" % (nf, len(znil)) if not znil: raise Exception("no images inside cbz") using = sorted(znil)[0][1].filename if znil: t += ", using " + using log(t) fi = zf.open(using) elif pk == "epub": fi = get_cover_from_epub(log, abspath) assert fi # !rm else: raise Exception("unknown compression %s" % (pk,)) fsz = 0 with os.fdopen(fd, "wb") as fo: fd = 0 while True: buf = fi.read(32768) if not buf: break fsz += len(buf) if fsz > maxsz: raise Exception("zipbomb defused") fo.write(buf) return ret except Exception as ex: if fd: os.close(fd) if ret: t = "failed to decompress file %r: %r" log(t % (abspath, ex)) wunlink(log, ret, vn.flags if vn else VF_CAREFUL) return "" return abspath def ffprobe( abspath: str, timeout: int = 60 ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: cmd = [ b"ffprobe", b"-hide_banner", b"-show_streams", b"-show_format", b"--", fsenc(abspath), ] rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200) retchk(rc, cmd, se) return parse_ffprobe(so) def parse_ffprobe( txt: str, ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: """ txt: output from ffprobe -show_format -show_streams returns: * normalized tags * original/raw tags * list of streams * format props """ streams = [] fmt = {} g = {} for ln in [x.rstrip("\r") for x in txt.split("\n")]: try: sk, sv = ln.split("=", 1) g[sk] = sv continue except: pass if ln == "[STREAM]": g = {} streams.append(g) if ln == "[FORMAT]": g = {"codec_type": "format"} # heh fmt = g streams = [fmt] + streams ret: dict[str, Any] = {} # processed md: dict[str, list[Any]] = {} # raw tags is_audio = fmt.get("format_name") in FMT_AU if fmt.get("filename", "").split(".")[-1].lower() in M4A: is_audio = True # if audio file, ensure audio stream appears first if ( is_audio and len(streams) > 2 and streams[1].get("codec_type") != "audio" and streams[2].get("codec_type") == "audio" ): streams = [fmt, streams[2], streams[1]] + streams[3:] have = {} for strm in streams: typ = strm.get("codec_type") if typ in have: continue have[typ] = True kvm = [] if typ == "audio": kvm = [ ["codec_name", "ac"], ["channel_layout", "chs"], ["sample_rate", ".hz"], ["bit_rate", ".aq"], ["bits_per_sample", ".bps"], ["bits_per_raw_sample", ".bprs"], ["duration", ".dur"], ] if typ == "video": if strm.get("DISPOSITION:attached_pic") == "1" or is_audio: continue kvm = [ ["codec_name", "vc"], ["pix_fmt", "pixfmt"], ["r_frame_rate", ".fps"], ["bit_rate", ".vq"], ["width", ".resw"], ["height", ".resh"], ["duration", ".dur"], ] if typ == "format": kvm = [["duration", ".dur"], ["bit_rate", ".q"], ["format_name", "fmt"]] for sk, rk in kvm: v1 = strm.get(sk) if v1 is None: continue if rk.startswith("."): try: zf = float(v1) v2 = ret.get(rk) if v2 is None or zf > v2: ret[rk] = zf except: # sqlite doesnt care but the code below does if v1 not in ["N/A"]: ret[rk] = v1 else: ret[rk] = v1 if ret.get("vc") == "ansi": # shellscript return {}, {}, [], {} for strm in streams: for sk, sv in strm.items(): if not sk.startswith("TAG:"): continue sk = sk[4:].strip() sv = sv.strip() if sk and sv and sk not in md: md[sk] = [sv] for sk in [".q", ".vq", ".aq"]: if sk in ret: ret[sk] /= 1000 # bit_rate=320000 for sk in [".q", ".vq", ".aq", ".resw", ".resh"]: if sk in ret: ret[sk] = int(ret[sk]) if ".fps" in ret: fps = ret[".fps"] if "/" in fps: fa, fb = fps.split("/") try: fps = float(fa) / float(fb) except: fps = 9001 if fps < 1000 and fmt.get("format_name") not in ["image2", "png_pipe"]: ret[".fps"] = round(fps, 3) else: del ret[".fps"] if ".dur" in ret: if ret[".dur"] < 0.1: del ret[".dur"] if ".q" in ret: del ret[".q"] if "fmt" in ret: ret["fmt"] = ret["fmt"].split(",")[0] if ".resw" in ret and ".resh" in ret: ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"]) zero = int("0") zd = {k: (zero, v) for k, v in ret.items()} return zd, md, streams, fmt def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]: import zipfile from .dxml import parse_xml try: from urlparse import urljoin # type: ignore # Python2 except ImportError: from urllib.parse import urljoin # Python3 with zipfile.ZipFile(abspath, "r") as z: # First open the container file to find the package document (.opf file) try: container_root = parse_xml(z.read("META-INF/container.xml").decode()) except KeyError: log("epub: no container file found in %s" % (abspath,)) return None # https://www.w3.org/TR/epub-33/#sec-container.xml-rootfile-elem container_ns = {"": "urn:oasis:names:tc:opendocument:xmlns:container"} # One file could contain multiple package documents, default to the first one rootfile_path = container_root.find("./rootfiles/rootfile", container_ns).get( "full-path" ) # Then open the first package document to find the path of the cover image try: package_root = parse_xml(z.read(rootfile_path).decode()) except KeyError: log("epub: no package document found in %s" % (abspath,)) return None # https://www.w3.org/TR/epub-33/#sec-package-doc package_ns = {"": "http://www.idpf.org/2007/opf"} # https://www.w3.org/TR/epub-33/#sec-cover-image coverimage_path_node = package_root.find( "./manifest/item[@properties='cover-image']", package_ns ) if coverimage_path_node is not None: coverimage_path = coverimage_path_node.get("href") else: # This might be an EPUB2 file, try the legacy way of specifying covers coverimage_path = _get_cover_from_epub2(log, package_root, package_ns) if not coverimage_path: raise Exception("no cover inside epub") # This url is either absolute (in the .epub) or relative to the package document adjusted_cover_path = urljoin(rootfile_path, coverimage_path) try: return z.open(adjusted_cover_path) except KeyError: t = "epub: cover specified in package document, but doesn't exist: %s" log(t % (adjusted_cover_path,)) def _get_cover_from_epub2( log: "NamedLogger", package_root, package_ns ) -> Optional[str]: # in , then # in xn = package_root.find("./metadata/meta[@name='cover']", package_ns) cover_id = xn.get("content") if xn is not None else None if not cover_id: return None for node in package_root.iterfind("./manifest/item", package_ns): if node.get("id") == cover_id: cover_path = node.get("href") return cover_path return None class MTag(object): def __init__(self, log_func: "RootLogger", args: argparse.Namespace) -> None: self.log_func = log_func self.args = args self.usable = True self.prefer_mt = not args.no_mtag_ff self.backend = ( "ffprobe" if args.no_mutagen or (HAVE_FFPROBE and EXE) else "mutagen" ) self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff self.read_xattrs = args.have_db_xattr self.get = self._get_xattr if self.read_xattrs else self._get_main mappings = args.mtm or_ffprobe = " or FFprobe" if self.backend == "mutagen": self._get = self.get_mutagen if not HAVE_MUTAGEN: self.log("could not load Mutagen, trying FFprobe instead", c=3) self.backend = "ffprobe" if self.backend == "ffprobe": self.usable = self.can_ffprobe self._get = self.get_ffprobe self.prefer_mt = True if not HAVE_FFPROBE: pass elif args.no_mtag_ff: msg = "found FFprobe but it was disabled by --no-mtag-ff" self.log(msg, c=3) if self.read_xattrs and not self.usable: t = "don't have the necessary dependencies to read conventional media tags, but will read xattrs" self.log(t) self.usable = True if not self.usable: self._get = None if EXE: t = "copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: " self.log(t + FFMPEG_URL) return msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n" pyname = os.path.basename(pybin) self.log(msg.format(or_ffprobe, " " * 37, pyname), c=1) return # https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html tagmap = { "album": ["album", "talb", "\u00a9alb", "original-album", "toal"], "artist": [ "artist", "tpe1", "\u00a9art", "composer", "performer", "arranger", "\u00a9wrt", "tcom", "tpe3", "original-artist", "tope", ], "title": ["title", "tit2", "\u00a9nam"], "circle": [ "album-artist", "tpe2", "aart", "organization", "band", ], ".tn": ["tracknumber", "trck", "trkn", "track"], "genre": ["genre", "tcon", "\u00a9gen"], "tdate": [ "original-release-date", "release-date", "date", "tdrc", "\u00a9day", "original-date", "original-year", "tyer", "tdor", "tory", "year", "creation-time", ], ".bpm": ["bpm", "tbpm", "tmpo", "tbp"], "key": ["initial-key", "tkey", "key"], "comment": ["comment", "comm", "\u00a9cmt", "comments", "description"], } if mappings: for k, v in [x.split("=") for x in mappings]: tagmap[k] = v.split(",") self.tagmap = {} for k, vs in tagmap.items(): vs2 = [] for v in vs: if "-" not in v: vs2.append(v) continue vs2.append(v.replace("-", " ")) vs2.append(v.replace("-", "_")) vs2.append(v.replace("-", "")) self.tagmap[k] = vs2 self.rmap = { v: [n, k] for k, vs in self.tagmap.items() for n, v in enumerate(vs) } # self.get = self.compare def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func("mtag", msg, c) def normalize_tags( self, parser_output: dict[str, tuple[int, Any]], md: dict[str, list[Any]] ) -> dict[str, Union[str, float]]: for sk, tv in dict(md).items(): if not tv: continue sk = sk.lower().split("::")[0].strip() key_mapping = self.rmap.get(sk) if not key_mapping: continue priority, alias = key_mapping if alias not in parser_output or parser_output[alias][0] > priority: parser_output[alias] = (priority, tv[0]) # take first value (lowest priority / most preferred) ret: dict[str, Union[str, float]] = { sk: unicode(tv[1]).strip() for sk, tv in parser_output.items() } # track 3/7 => track 3 for sk, zv in ret.items(): if sk[0] == ".": sv = str(zv).split("/")[0].strip().lstrip("0") ret[sk] = sv or 0 # normalize key notation to rekobo okey = ret.get("key") if okey: key = str(okey).replace(" ", "").replace("maj", "").replace("min", "m") ret["key"] = REKOBO_LKEY.get(key.lower(), okey) if self.args.mtag_vv: zl = " ".join("\033[36m{} \033[33m{}".format(k, v) for k, v in ret.items()) self.log("norm: {}\033[0m".format(zl), "90") return ret def compare(self, abspath: str) -> dict[str, Union[str, float]]: if abspath.endswith(".au"): return {} print("\n" + abspath) r1 = self.get_mutagen(abspath) r2 = self.get_ffprobe(abspath) keys = {} for d in [r1, r2]: for k in d.keys(): keys[k] = True diffs = [] l1 = [] l2 = [] for k in sorted(keys.keys()): if k in [".q", ".dur"]: continue # lenient v1 = r1.get(k) v2 = r2.get(k) if v1 == v2: print(" ", k, v1) elif v1 != "0000": # FFprobe date=0 diffs.append(k) print(" 1", k, v1) print(" 2", k, v2) if v1: l1.append(k) if v2: l2.append(k) if diffs: raise Exception() return r1 def _get_xattr( self, abspath: str, vf: dict[str, Any] ) -> dict[str, Union[str, float]]: ret = self._get_main(abspath, vf) if self._get else {} if "db_xattr_no" in vf: try: neg = vf["db_xattr_no"] zsl = os.listxattr(abspath) zsl = [x for x in zsl if x not in neg] for xattr in zsl: zb = os.getxattr(abspath, xattr) ret[xattr] = zb.decode("utf-8", "replace") except: self.log("failed to read xattrs from [%s]\n%s" % (abspath, min_ex()), 3) elif "db_xattr_yes" in vf: for xattr in vf["db_xattr_yes"]: if "=" in xattr: xattr, name = xattr.split("=", 1) else: name = xattr try: zs = os.getxattr(abspath, xattr) ret[name] = zs.decode("utf-8", "replace") except: pass return ret def _get_main( self, abspath: str, vf: dict[str, Any] ) -> dict[str, Union[str, float]]: ext = abspath.split(".")[-1].lower() if ext not in self.args.au_unpk: return self._get(abspath) ap = au_unpk(self.log, self.args.au_unpk, abspath) if not ap: return {} ret = self._get(ap) if ap != abspath: wunlink(self.log, ap, VF_CAREFUL) return ret def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]: ret: dict[str, tuple[int, Any]] = {} if not bos.path.isfile(abspath): return {} from mutagen import File try: md = File(fsenc(abspath), easy=True) assert md if self.args.mtag_vv: for zd in (md.info.__dict__, dict(md.tags)): zl = ["\033[36m{} \033[33m{}".format(k, v) for k, v in zd.items()] self.log("mutagen: {}\033[0m".format(" ".join(zl)), "90") if not md.info.length and not md.info.codec: raise Exception() except Exception as ex: if self.args.mtag_v: self.log("mutagen-err [%s] @ %r" % (ex, abspath), "90") return self.get_ffprobe(abspath) if self.can_ffprobe else {} sz = bos.path.getsize(abspath) try: ret[".q"] = (0, int((sz / md.info.length) / 128)) except: pass for attr, k, norm in [ ["codec", "ac", unicode], ["channels", "chs", int], ["sample_rate", ".hz", int], ["bitrate", ".aq", int], ["length", ".dur", int], ]: try: v = getattr(md.info, attr) except: if k != "ac": continue try: v = str(md.info).split(".")[1] if v.startswith("ogg"): v = v[3:] except: continue if not v: continue if k == ".aq": v /= 1000 # type: ignore if k == "ac" and v.startswith("mp4a.40."): v = "aac" ret[k] = (0, norm(v)) return self.normalize_tags(ret, md) def get_ffprobe(self, abspath: str) -> dict[str, Union[str, float]]: if not bos.path.isfile(abspath): return {} ret, md, _, _ = ffprobe(abspath, self.args.mtag_to) if self.args.mtag_vv: for zd in (ret, dict(md)): zl = ["\033[36m{} \033[33m{}".format(k, v) for k, v in zd.items()] self.log("ffprobe: {}\033[0m".format(" ".join(zl)), "90") return self.normalize_tags(ret, md) def get_bin( self, parsers: dict[str, MParser], abspath: str, oth_tags: dict[str, Any] ) -> dict[str, Any]: if not bos.path.isfile(abspath): return {} env = os.environ.copy() try: if EXE: raise Exception() pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) zsl = [str(pypath)] + [str(x) for x in sys.path if x] pypath = str(os.pathsep.join(zsl)) env["PYTHONPATH"] = pypath except: raise # might be expected outside cpython ext = abspath.split(".")[-1].lower() if ext in self.args.au_unpk: ap = au_unpk(self.log, self.args.au_unpk, abspath) else: ap = abspath ret: dict[str, Any] = {} if not ap: return ret for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])): try: cmd = [parser.bin, ap] if parser.bin.endswith(".py"): cmd = [pybin] + cmd args = { "env": env, "nice": True, "oom": 300, "timeout": parser.timeout, "kill": parser.kill, "capture": parser.capture, } if parser.pri: zd = oth_tags.copy() zd.update(ret) args["sin"] = json.dumps(zd).encode("utf-8", "replace") bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])] rc, v, err = runcmd(bcmd, **args) # type: ignore retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v) v = v.strip() if not v: continue if "," not in tagname: ret[tagname] = v else: zj = json.loads(v) for tag in tagname.split(","): if tag and tag in zj: ret[tag] = zj[tag] except: if self.args.mtag_v: t = "mtag error: tagname %r, parser %r, file %r => %r" self.log(t % (tagname, parser.bin, abspath, min_ex()), 6) if ap != abspath: wunlink(self.log, ap, VF_CAREFUL) return ret ================================================ FILE: copyparty/multicast.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import socket import time import ipaddress from ipaddress import ( IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network, ) from .__init__ import MACOS, TYPE_CHECKING from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack if TYPE_CHECKING: from .svchub import SvcHub if True: # pylint: disable=using-constant-test from typing import Optional, Union if not hasattr(socket, "IPPROTO_IPV6"): setattr(socket, "IPPROTO_IPV6", 41) class NoIPs(Exception): pass class MC_Sck(object): """there is one socket for each server ip""" def __init__( self, sck: socket.socket, nd: Netdev, grp: str, ip: str, net: Union[IPv4Network, IPv6Network], ): self.sck = sck self.idx = nd.idx self.name = nd.name self.grp = grp self.mreq = b"" self.ip = ip self.net = net self.ips = {ip: net} self.v6 = ":" in ip self.have4 = ":" not in ip self.have6 = ":" in ip class MCast(object): def __init__( self, hub: "SvcHub", Srv: type[MC_Sck], on: list[str], off: list[str], mc_grp_4: str, mc_grp_6: str, port: int, vinit: bool, ) -> None: """disable ipv%d by setting mc_grp_%d empty""" self.hub = hub self.Srv = Srv self.args = hub.args self.asrv = hub.asrv self.log_func = hub.log self.on = on self.off = off self.grp4 = mc_grp_4 self.grp6 = mc_grp_6 self.port = port self.vinit = vinit self.srv: dict[socket.socket, MC_Sck] = {} # listening sockets self.sips: set[str] = set() # all listening ips (including failed attempts) self.ll_ok: set[str] = set() # fallback linklocal IPv4 and IPv6 addresses self.b2srv: dict[bytes, MC_Sck] = {} # binary-ip -> server socket self.b4: list[bytes] = [] # sorted list of binary-ips self.b6: list[bytes] = [] # sorted list of binary-ips self.cscache: dict[str, Optional[MC_Sck]] = {} # client ip -> server cache self.running = True def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func("multicast", msg, c) def create_servers(self) -> list[str]: bound: list[str] = [] netdevs = self.hub.tcpsrv.netdevs blist = self.hub.tcpsrv.bound if self.args.http_no_tcp: blist = self.hub.tcpsrv.seen_eps ips = [x[0] for x in blist] if "::" in ips: ips = [x for x in ips if x != "::"] + list( [x.split("/")[0] for x in netdevs if ":" in x] ) ips.append("0.0.0.0") if "0.0.0.0" in ips: ips = [x for x in ips if x != "0.0.0.0"] + list( [x.split("/")[0] for x in netdevs if ":" not in x] ) ips = [x for x in ips if x not in ("::1", "127.0.0.1")] ips = find_prefix(ips, list(netdevs)) on = self.on[:] off = self.off[:] for lst in (on, off): for av in list(lst): try: arg_net = ip_network(av, False) except: arg_net = None for sk, sv in netdevs.items(): if arg_net: net_ip = ip_address(sk.split("/")[0]) if net_ip in arg_net and sk not in lst: lst.append(sk) if (av == str(sv.idx) or av == sv.name) and sk not in lst: lst.append(sk) if on: ips = [x for x in ips if x in on] elif off: ips = [x for x in ips if x not in off] if not self.grp4: ips = [x for x in ips if ":" in x] if not self.grp6: ips = [x for x in ips if ":" not in x] ips = list(set(ips)) all_selected = ips[:] # discard non-linklocal ipv6 ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)] if not ips: raise NoIPs() for ip in ips: v6 = ":" in ip netdev = netdevs[ip] if not netdev.idx: t = "using INADDR_ANY for ip [{}], netdev [{}]" if not self.srv and ip not in ["::", "0.0.0.0"]: self.log(t.format(ip, netdev), 3) ipv = socket.AF_INET6 if v6 else socket.AF_INET sck = socket.socket(ipv, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sck.settimeout(None) sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: # safe for this purpose; https://lwn.net/Articles/853637/ sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except: pass # most ipv6 clients expect multicast on linklocal ip only; # add a/aaaa records for the other nic IPs other_ips: set[str] = set() if v6: for nd in netdevs.values(): if nd.idx == netdev.idx and nd.ip in all_selected and ":" in nd.ip: other_ips.add(nd.ip) net = ipaddress.ip_network(ip, False) ip = ip.split("/")[0] srv = self.Srv(sck, netdev, self.grp6 if ":" in ip else self.grp4, ip, net) for oth_ip in other_ips: srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False) # gvfs breaks if a linklocal ip appears in a dns reply ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)} rt = {k: v for k, v in srv.ips.items() if k not in ll} if self.args.ll or not rt: self.ll_ok.update(list(ll)) if not self.args.ll: srv.ips = rt or ll if not srv.ips: self.log("no IPs on {}; skipping [{}]".format(netdev, ip), 3) continue try: self.setup_socket(srv) self.srv[sck] = srv bound.append(ip) except: t = "announce failed on {} [{}]:\n{}" self.log(t.format(netdev, ip, min_ex()), 3) sck.close() if self.args.zm_msub: for s1 in self.srv.values(): for s2 in self.srv.values(): if s1.idx != s2.idx: continue if s1.ip not in s2.ips: s2.ips[s1.ip] = s1.net if self.args.zm_mnic: for s1 in self.srv.values(): for s2 in self.srv.values(): for ip1, net1 in list(s1.ips.items()): for ip2, net2 in list(s2.ips.items()): if net1 == net2 and ip1 != ip2: s1.ips[ip2] = net2 self.sips = set([x.split("/")[0] for x in all_selected]) for srv in self.srv.values(): assert srv.ip in self.sips Daemon(self.hopper, "mc-hop") return bound def setup_socket(self, srv: MC_Sck) -> None: sck = srv.sck if srv.v6: if self.vinit: zsl = list(srv.ips.keys()) self.log("v6({}) idx({}) {}".format(srv.ip, srv.idx, zsl), 6) for ip in srv.ips: bip = socket.inet_pton(socket.AF_INET6, ip) self.b2srv[bip] = srv self.b6.append(bip) grp = self.grp6 if srv.idx else "" try: if MACOS: raise Exception() sck.bind((grp, self.port, 0, srv.idx)) except: sck.bind(("", self.port, 0, srv.idx)) bgrp = socket.inet_pton(socket.AF_INET6, self.grp6) dev = spack(b"@I", srv.idx) srv.mreq = bgrp + dev if srv.idx != socket.INADDR_ANY: sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, dev) try: sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255) sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1) except: # macos t = "failed to set IPv6 TTL/LOOP; announcements may not survive multiple switches/routers" self.log(t, 3) else: if self.vinit: self.log("v4({}) idx({})".format(srv.ip, srv.idx), 6) bip = socket.inet_aton(srv.ip) self.b2srv[bip] = srv self.b4.append(bip) grp = self.grp4 if srv.idx else "" try: if MACOS: raise Exception() sck.bind((grp, self.port)) except: sck.bind(("", self.port)) bgrp = socket.inet_aton(self.grp4) dev = ( spack(b"=I", socket.INADDR_ANY) if srv.idx == socket.INADDR_ANY else socket.inet_aton(srv.ip) ) srv.mreq = bgrp + dev if srv.idx != socket.INADDR_ANY: sck.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, dev) try: sck.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) sck.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) except: # probably can't happen but dontcare if it does t = "failed to set IPv4 TTL/LOOP; announcements may not survive multiple switches/routers" self.log(t, 3) if self.hop(srv, False): self.log("igmp was already joined?? chilling for a sec", 3) time.sleep(1.2) self.hop(srv, True) self.b4.sort(reverse=True) self.b6.sort(reverse=True) def hop(self, srv: MC_Sck, on: bool) -> bool: """rejoin to keepalive on routers/switches without igmp-snooping""" sck = srv.sck req = srv.mreq if ":" in srv.ip: if not on: try: sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, req) return True except: return False else: sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, req) else: if not on: try: sck.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, req) return True except: return False else: # t = "joining {} from ip {} idx {} with mreq {}" # self.log(t.format(srv.grp, srv.ip, srv.idx, repr(srv.mreq)), 6) sck.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req) return True def hopper(self): while self.args.mc_hop and self.running: time.sleep(self.args.mc_hop) if not self.running: return for srv in self.srv.values(): self.hop(srv, False) # linux does leaves/joins twice with 0.2~1.05s spacing time.sleep(1.2) if not self.running: return for srv in self.srv.values(): self.hop(srv, True) def map_client(self, cip: str) -> Optional[MC_Sck]: try: return self.cscache[cip] except: pass ret: Optional[MC_Sck] = None v6 = ":" in cip ci = IPv6Address(cip) if v6 else IPv4Address(cip) for x in self.b6 if v6 else self.b4: srv = self.b2srv[x] if any([x for x in srv.ips.values() if ci in x]): ret = srv break if not ret and cip in ("127.0.0.1", "::1"): # just give it something ret = list(self.srv.values())[0] if not ret and cip.startswith("169.254"): # idk how to map LL IPv4 msgs to nics; # just pick one and hope for the best lls = ( x for x in self.srv.values() if next((y for y in x.ips if y in self.ll_ok), None) ) ret = next(lls, None) if ret: t = "new client on {} ({}): {}" self.log(t.format(ret.name, ret.net, cip), 6) else: t = "could not map client {} to known subnet; maybe forwarded from another network?" self.log(t.format(cip), 3) if len(self.cscache) > 9000: self.cscache = {} self.cscache[cip] = ret return ret ================================================ FILE: copyparty/pwhash.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import base64 import hashlib import os import sys import threading from .__init__ import unicode try: if os.environ.get("PRTY_NO_ARGON2"): raise Exception() HAVE_ARGON2 = True from argon2 import exceptions as argon2ex except: HAVE_ARGON2 = False class PWHash(object): def __init__(self, args: argparse.Namespace): self.args = args zsl = args.ah_alg.split(",") zsl = [x.strip() for x in zsl] alg = zsl[0] if alg == "none": alg = "" self.alg = alg self.ac = zsl[1:] if not alg: self.on = False self.hash = unicode return self.on = True self.salt = args.ah_salt.encode("utf-8") self.cache: dict[str, str] = {} self.mutex = threading.Lock() self.hash = self._cache_hash if alg == "sha2": self._hash = self._gen_sha2 elif alg == "scrypt": self._hash = self._gen_scrypt elif alg == "argon2": self._hash = self._gen_argon2 else: t = "unsupported password hashing algorithm [{}], must be one of these: argon2 scrypt sha2 none" raise Exception(t.format(alg)) def _cache_hash(self, plain: str) -> str: with self.mutex: try: return self.cache[plain] except: pass if not plain: return "" if len(plain) > 255: raise Exception("password too long") if len(self.cache) > 9000: self.cache = {} ret = self._hash(plain) self.cache[plain] = ret return ret def _gen_sha2(self, plain: str) -> str: its = int(self.ac[0]) if self.ac else 424242 bplain = plain.encode("utf-8") ret = b"\n" for _ in range(its): ret = hashlib.sha512(self.salt + bplain + ret).digest() return "+" + base64.urlsafe_b64encode(ret[:24]).decode("utf-8") def _gen_scrypt(self, plain: str) -> str: cost = 2 << 13 its = 2 blksz = 8 para = 4 ramcap = 0 # openssl 1.1 = 32 MiB try: cost = 2 << int(self.ac[0]) its = int(self.ac[1]) blksz = int(self.ac[2]) para = int(self.ac[3]) ramcap = int(self.ac[4]) * 1024 * 1024 except: pass cfg = {"salt": self.salt, "n": cost, "r": blksz, "p": para, "dklen": 24} if ramcap: cfg["maxmem"] = ramcap ret = plain.encode("utf-8") for _ in range(its): ret = hashlib.scrypt(ret, **cfg) return "+" + base64.urlsafe_b64encode(ret).decode("utf-8") def _gen_argon2(self, plain: str) -> str: from argon2.low_level import Type as ArgonType from argon2.low_level import hash_secret time_cost = 3 mem_cost = 256 parallelism = 4 version = 19 try: time_cost = int(self.ac[0]) mem_cost = int(self.ac[1]) parallelism = int(self.ac[2]) version = int(self.ac[3]) except: pass bplain = plain.encode("utf-8") bret = hash_secret( secret=bplain, salt=self.salt, time_cost=time_cost, memory_cost=mem_cost * 1024, parallelism=parallelism, hash_len=24, type=ArgonType.ID, version=version, ) ret = bret.split(b"$")[-1].decode("utf-8") return "+" + ret.replace("/", "_").replace("+", "-") def stdin(self) -> None: while True: ln = sys.stdin.readline().strip() if not ln: break print(self.hash(ln)) def cli(self) -> None: import getpass if self.args.usernames: t = "since you have enabled --usernames, please provide username:password" print(t) while True: try: p1 = getpass.getpass("password> ") p2 = getpass.getpass("again or just hit ENTER> ") except EOFError: return if p2 and p1 != p2: print("\033[31minputs don't match; try again\033[0m", file=sys.stderr) continue print(self.hash(p1)) print() ================================================ FILE: copyparty/qrkode.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os try: if os.environ.get("PRTY_SYS_ALL") or os.environ.get("PRTY_SYS_QRCG"): raise ImportError() from .stolen.qrcodegen import QrCode qrgen = QrCode.encode_binary VENDORED = True except ImportError: VENDORED = False from qrcodegen import QrCode if os.environ.get("PRTY_MODSPEC"): from inspect import getsourcefile print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode)) if True: # pylint: disable=using-constant-test import typing from typing import Any, Optional, Sequence, Union if not VENDORED: def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode": ret = None V = QrCode.Ecc for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]: qr = QrCode.encode_binary(data, e) qr.size = qr._size qr.modules = qr._modules if not ret or ret.size > qr.size: ret = qr return ret qrgen = _qrgen def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str: tab = qr.modules sz = qr.size if sz % 2 and zoom == 1: tab.append([False] * sz) tab = [[False] * sz] * pad + tab + [[False] * sz] * pad tab = [[False] * pad + x + [False] * pad for x in tab] rows: list[str] = [] if zoom == 1: for y in range(0, len(tab), 2): row = "" for x in range(len(tab[y])): v = 2 if tab[y][x] else 0 v += 1 if tab[y + 1][x] else 0 row += " ▄▀█"[v] rows.append(row) else: for tr in tab: row = "" for zb in tr: row += " █"[int(zb)] * 2 rows.append(row) return "\n".join(rows) def qr2png( qr: QrCode, zoom: int, pad: int, bg: Optional[tuple[int, int, int]], fg: Optional[tuple[int, int, int]], ap: str, ) -> None: from PIL import Image tab = qr.modules sz = qr.size psz = sz + pad * 2 if bg: img = Image.new("RGB", (psz, psz), bg) else: img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0)) fg = (fg[0], fg[1], fg[2], 255) for y in range(sz): for x in range(sz): if tab[y][x]: img.putpixel((x + pad, y + pad), fg) if zoom != 1: img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST) img.save(ap) def qr2svg(qr: QrCode, border: int) -> str: parts: list[str] = [] for y in range(qr.size): sy = border + y for x in range(qr.size): if qr.modules[y][x]: parts.append("M%d,%dh1v1h-1z" % (border + x, sy)) t = """\ """ return t.format(qr.size + border * 2, " ".join(parts)) ================================================ FILE: copyparty/res/__init__.py ================================================ ================================================ FILE: copyparty/res/insecure.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDD84b31Brvmfcc GTwXuf5n5NW6KxIhLWVZgsCJq+lwGTGl1Cqm9vnzZqD7MDLNzztWk51FDXxeJy9Z Vos1F+n3qfDi4zdd8zWIJxLTsIEkqZjSWiQqqmFijXoaEPBsQ32qRZprh/FfNw3P TagroIgpgzd196eoWdDdggenYBG8/PbktaBKgOo6jz6Q5/UzNTmvcbtlxlsxPos8 aTqzezCgA/cArg8mxmfvRPrw8umC8ATiVLvGrXeMiSvcr8Els5air/idhYC8u5zO zGZCePxoKUptQvh/06jsye1S3JOf+RsfAWvu7DgWvSJU2dHfVmtb4ZMSa6cjqTPP cW7S1WyLAgMBAAECggEAAwabqvAHinOiMTjiiKtClnAeLMXFfeWpjvxJ5NZWwHhj H+Bq2DEwIuYOzlIsNqlgjTGyWAKhTQLl5EdF1wgLgNuK8LX5gOXkibmwvLwZAmvs BDOII3CGGHN+0zA3xjQ0mJCCle5/d6zt9amJU0MjVyDDlnrAiAT7CLCdVaRSIczv SETM90NBVHNMraB+Tz9kR1FhyDDlzqpRn/19Hm4BG6/iSmPYpL3jgCRWNweXSXdd x9EitJCz6Yo9+0qPD8DayrH5sKviZw0QDXUezWCcQvgXMsv5MJa7+UaawLk6bq5B Ou1TFGuFdNeNmWofAwPE8UOSIXoKhBK84z5lpTs44QKBgQDg9bRrn3a2u8vpx3r7 B+v4dlVq5I3lbVwLwEo6vnw2UiIOtQaQMiWX8HoHR3rmzCVMZq8xNNhEYSboFgtB bbQaxoApdqVTF700c5Le82tj3rBXN0vBIUm6Nzpz0IKQw8HWGz1q/sHd8uZdmtAO m4JrZIJqMItcLhyjYCp8hRUkvwKBgQDe/SZVCBMocHASyB4RgeAkNKSjFEYe2yAl 23W9Me3koJqTvHEZnv3Xk5O3HF0dxElktJW5slyrA2BcrzuusO+pge/29OV4ypFa kI51ebMgO3hm2BKKMwxR3mT4KVAbKveLi7hdlnbHVCcZelygkw+11tzIfaikeswR c4FoyPpvNQKBgDEA1OBsyCteFTlDnuJ4A0sIW+sBBnfnrplQtdq+C8i5c3nIrTlT 8yR52dskEv2bkrRl2dvaKxIaJ6N+yczi3MzIWLqvgavsC+cVFfVDCS2kIL2e6f2U Br9tsGnyDb8DJYJCRMq92/VBKDVTt+a2sV47cr02/eSClvJvzFF7m/N5AoGAJYzD k7YUY87rUH5aceBI+k/TGZMka7XCqB1Yqk9qHAHfhdlJwmK/pDm5ujAQjh6rrUWr oOWkLTgYVgM8LaKl+Qlke1Wp/rk92N5W3vlrbJYXJFpmZNdLz81/ezqZvrlxjhIt LbVUsyQ8oVG1n2SkVJ6l9y0R5QC4tIea1yZg5bECgYEAhF2EOtBCJ3EDEpHvnlK7 OuLRVjce4Vy+Mj3nFbHQMJf+A1B7+B43Ce1fn94OfGJldLDjvaZb8O91JAkHAwJQ GCpOVj9go0ffg/2hurtXW3bMLXIXqcn1538MB5NdlcL22+T0CCprAeuRQm4saMhz mkt5VszEuF/c88usk0ZMm4Y= -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDNTCCAh2gAwIBAgIUWWDVQNmrPPo2HlyhYuSYY2uKPbEwDQYJKoZIhvcNAQEL BQAwKjELMAkGA1UEBhMCTk8xGzAZBgNVBAMMEmNvcHlwYXJ0eS1pbnNlY3VyZTAe Fw0wMDAxMDEwMDAwMDBaFw0zODAxMTkwMDAwMDBaMCoxCzAJBgNVBAYTAk5PMRsw GQYDVQQDDBJjb3B5cGFydHktaW5zZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQDD84b31BrvmfccGTwXuf5n5NW6KxIhLWVZgsCJq+lwGTGl1Cqm 9vnzZqD7MDLNzztWk51FDXxeJy9ZVos1F+n3qfDi4zdd8zWIJxLTsIEkqZjSWiQq qmFijXoaEPBsQ32qRZprh/FfNw3PTagroIgpgzd196eoWdDdggenYBG8/PbktaBK gOo6jz6Q5/UzNTmvcbtlxlsxPos8aTqzezCgA/cArg8mxmfvRPrw8umC8ATiVLvG rXeMiSvcr8Els5air/idhYC8u5zOzGZCePxoKUptQvh/06jsye1S3JOf+RsfAWvu 7DgWvSJU2dHfVmtb4ZMSa6cjqTPPcW7S1WyLAgMBAAGjUzBRMB0GA1UdDgQWBBQC uhBjOit55cEz7jMIiJq2BljqxDAfBgNVHSMEGDAWgBQCuhBjOit55cEz7jMIiJq2 BljqxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCu7VE/Pt4H IOHAacQFJN9rdUtI1lcvbJEMdpPglHgFa546pgzDjS8QGvULmppb4mkMbezZ2u+v ZsWMfLxATYgfqEPO/fnT/UqFX2TI2pmmZhqSV+eW8AGRPD5LYyfXpvU4ciSz4Fyl FlrcCOStbeOjHwpeF+n92sF6/LL3n4yy1jmLnTaZDdd59PcWpQkX8omBWlXPdCPR ot0ZI2OpK5mDUG2FglCXl29k1q/zLsKPyfmG7T6VOMB8HO7EylzgSTke4WH9QLR/ bkn7AZU+JEIdC6SgR0WSQAntqM+b/OuYSBzXDX0XLwnyY2Dx8SshKNF9v6GwTq7j yQev95xStUTw -----END CERTIFICATE----- ================================================ FILE: copyparty/sftpd.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import errno import hashlib import logging import os import select import socket import time from threading import ExceptHookArgs import paramiko import paramiko.common import paramiko.sftp_attr from paramiko.common import AUTH_FAILED, AUTH_SUCCESSFUL from paramiko.sftp import ( SFTP_FAILURE, SFTP_NO_SUCH_FILE, SFTP_OK, SFTP_OP_UNSUPPORTED, SFTP_PERMISSION_DENIED, ) from .__init__ import ANYWIN, TYPE_CHECKING from .authsrv import LEELOO_DALLAS, VFS, AuthSrv from .bos import bos from .util import ( VF_CAREFUL, Daemon, ODict, Pebkac, ipnorm, min_ex, read_utf8, relchk, runhook, sanitize_fn, ub64enc, undot, vjoin, wunlink, ) if TYPE_CHECKING: from .svchub import SvcHub if True: # pylint: disable=using-constant-test from typing import Any, BinaryIO, Optional, Union SATTR = paramiko.sftp_attr.SFTPAttributes class SSH_Srv(paramiko.ServerInterface): def __init__(self, hub: "SvcHub", addr: Any): self.hub = hub self.args = args = hub.args self.log_func = hub.log self.uname = "*" self.addr = addr self.ip = addr[0] if self.ip.startswith("::ffff:"): self.ip = self.ip[7:] zsl = [] if args.sftp_anon: zsl.append("none") if args.sftp_key2u: zsl.append("publickey") if args.sftp_pw or args.sftp_anon: zsl.append("password") self._auths = ",".join(zsl) def log(self, msg: str, c: Union[int, str] = 0) -> None: self.hub.log("sftp:%s" % (self.ip,), msg, c) def get_allowed_auths(self, username: str) -> str: return self._auths def get_banner(self) -> tuple[Optional[str], Optional[str]]: if self.args.sftpv: self.log("get_banner") t = self.args.sftp_banner if not t: return (None, None) if t.startswith("@"): t = read_utf8(self.log, t[1:], False) if t and not t.endswith("\n"): t += "\n" return (t, "en-US") def check_channel_request(self, kind: str, chanid: int) -> int: if self.args.sftpv: self.log("channel-request: %r, %r" % (kind, chanid)) if kind == "session": return paramiko.common.OPEN_SUCCEEDED return paramiko.common.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_none(self, username: str) -> int: try: return self._check_auth_none(username) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return AUTH_FAILED def _check_auth_none(self, uname: str) -> int: args = self.args if uname != args.sftp_anon or not uname: return AUTH_FAILED ipn = ipnorm(self.ip) bans = self.hub.bans if ipn in bans: rt = bans[ipn] - time.time() if rt < 0: self.log("client unbanned") del bans[ipn] else: self.log("client is banned") return AUTH_FAILED self.uname = "*" self.log("auth-none OK: *") return AUTH_SUCCESSFUL def check_auth_password(self, username: str, password: str) -> int: try: return self._check_auth_password(username, password) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return AUTH_FAILED def _check_auth_password(self, uname: str, pw: str) -> int: args = self.args if args.sftpv: logpw = pw if args.log_badpwd == 0: logpw = "" elif args.log_badpwd == 2: zb = hashlib.sha512(pw.encode("utf-8", "replace")).digest() logpw = "%" + ub64enc(zb[:12]).decode("ascii") self.log("auth-pw: %r, %r" % (uname, logpw)) ipn = ipnorm(self.ip) bans = self.hub.bans if ipn in bans: rt = bans[ipn] - time.time() if rt < 0: self.log("client unbanned") del bans[ipn] else: self.log("client is banned") return AUTH_FAILED anon = args.sftp_anon if anon and uname == anon: self.uname = "*" self.log("auth-pw OK: *") return AUTH_SUCCESSFUL if not args.sftp_pw: return AUTH_FAILED if args.usernames: alts = ["%s:%s" % (uname, pw)] else: alts = [pw, uname] attempt = "%s:%s" % (uname, pw) uname = "" asrv = self.hub.asrv for zs in alts: zs = asrv.iacct.get(asrv.ah.hash(zs), "") if zs: uname = zs break if args.ipu and uname == "*": uname = args.ipu_iu[args.ipu_nm.map(self.ip)] if args.ipr and uname in args.ipr_u: if not args.ipr_u[uname].map(self.ip): logging.warning("username [%s] rejected by --ipr", uname) return AUTH_FAILED if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): g = self.hub.gpwd if g.lim: bonk, ip = g.bonk(self.ip, attempt) if bonk: logging.warning("client banned: invalid passwords") bans[self.ip] = bonk try: # only possible if multiprocessing disabled self.hub.broker.httpsrv.bans[ip] = bonk # type: ignore self.hub.broker.httpsrv.nban += 1 # type: ignore except: pass return AUTH_FAILED self.uname = uname self.log("auth-pw OK: %s" % (uname,)) return AUTH_SUCCESSFUL def check_auth_publickey(self, username: str, key: paramiko.PKey) -> int: try: return self._check_auth_publickey(username, key) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return AUTH_FAILED def _check_auth_publickey(self, uname: str, key: paramiko.PKey) -> int: args = self.args if args.sftpv: zs = key.get_name() + "," + key.get_base64()[:32] self.log("auth-key: %r, %r" % (uname, zs)) ipn = ipnorm(self.ip) bans = self.hub.bans if ipn in bans: rt = bans[ipn] - time.time() if rt < 0: self.log("client unbanned") del bans[ipn] else: self.log("client is banned") return AUTH_FAILED anon = args.sftp_anon if anon and uname == anon: self.uname = "*" self.log("auth-key OK: *") return AUTH_SUCCESSFUL attempt = "%s %s" % (key.get_name(), key.get_base64()) ok = args.sftp_key2u.get(attempt) == uname if ok and args.ipr and uname in args.ipr_u: if not args.ipr_u[uname].map(self.ip): logging.warning("username [%s] rejected by --ipr", uname) return AUTH_FAILED asrv = self.hub.asrv if not ok or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): self.log("auth-key REJECTED: %s" % (uname,)) return AUTH_FAILED self.uname = uname self.log("auth-key OK: %s" % (uname,)) return AUTH_SUCCESSFUL class SFTP_FH(paramiko.SFTPHandle): def __init__(self, flags: int = 0) -> None: self.filename = "" self.readfile: Optional[BinaryIO] = None self.writefile: Optional[BinaryIO] = None super(SFTP_FH, self).__init__(flags) def stat(self): try: f = self.readfile or self.writefile return SATTR.from_stat(os.fstat(f.fileno())) except OSError as ex: return paramiko.SFTPServer.convert_errno(ex.errno) def chattr(self, attr): # python doesn't have equivalents to fchown or fchmod, so we have to # use the stored filename if not self.writefile: return SFTP_PERMISSION_DENIED try: paramiko.SFTPServer.set_file_attr(self.filename, attr) return SFTP_OK except OSError as ex: return paramiko.SFTPServer.convert_errno(ex.errno) class SFTP_Srv(paramiko.SFTPServerInterface): def __init__(self, ssh: paramiko.ServerInterface, *a, **ka): super(SFTP_Srv, self).__init__(ssh, *a, **ka) self.ssh = ssh self.ip: str = ssh.ip # type: ignore self.hub: "SvcHub" = ssh.hub # type: ignore self.uname: str = ssh.uname # type: ignore self.args = self.hub.args self.asrv: "AuthSrv" = self.hub.asrv self.v = self.args.sftpv self.vv = self.args.sftpvv if self.uname == LEELOO_DALLAS: raise Exception("send her back") self.vols = [ vp for vp, vn in self.asrv.vfs.all_vols.items() if self.uname in vn.axs.uread or self.uname in vn.axs.uwrite or self.uname in vn.axs.uget ] self.vis = set() for zs in self.vols: self.vis.add(zs) while zs: zs = zs.rsplit("/", 1)[0] if "/" in zs else "" self.vis.add(zs) def log(self, msg: str, c: Union[int, str] = 0) -> None: self.hub.log("sftp:%s" % (self.ip,), msg, c) def v2a( self, vpath: str, r: bool = False, w: bool = False, m: bool = False, d: bool = False, ) -> tuple[str, VFS, str]: vpath = vpath.replace(os.sep, "/").strip("/") rd, fn = os.path.split(vpath) if relchk(rd): self.log("malicious vpath: %s", vpath) raise Exception("Unsupported characters in [%s]" % (vpath,)) fn = sanitize_fn(fn or "") vpath = vjoin(rd, fn) vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) if ( w and fn.lower() in vn.flags["emb_all"] and self.uname not in vn.axs.uread and "wo_up_readme" not in vn.flags ): fn = "_wo_" + fn vpath = vjoin(rd, fn) vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) if not vn.realpath: # return "", vn, rem raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,)) if "xdev" in vn.flags or "xvol" in vn.flags: ap = vn.canonical(rem) avn = vn.chk_ap(ap) t = "Permission denied in [{}]" if not avn: raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,)) cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname] if r and not cr or w and not cw or m and not cm or d and not cd: raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,)) else: ap = vn.canonical(rem, False) if "bcasechk" in vn.flags and not vn.casechk(rem, True): raise OSError(errno.ENOENT, "file does not exist case-sensitively") return ap, vn, rem def list_folder(self, path: str) -> list[SATTR] | int: try: return self._list_folder(path) except Pebkac as ex: if ex.code == 404: self.log("folder 404: %s" % (path,)) return SFTP_NO_SUCH_FILE return SFTP_PERMISSION_DENIED except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _list_folder(self, path: str) -> list[SATTR] | int: if self.v: self.log("ls(%s):" % (path,)) path = path.strip("/") try: ap, vn, rem = self.v2a(path, r=True) except Pebkac: try: self.v2a(path, w=True) self.log("ls(%s): [] (write-only)" % (path,)) return [] # display write-only folders as empty except: pass if path not in self.vis: self.log("ls(%s): EPERM" % (path,)) return SFTP_PERMISSION_DENIED # list of accessible volumes ret = [] zi = int(time.time()) vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi)) prefix = path + "/" for vn in self.asrv.vfs.all_nodes.values(): if path and not vn.vpath.startswith(prefix): continue # vn is parent vname = vn.vpath[len(prefix) :] if "/" in vname or not vname: continue # only include vols at current level ret.append(SATTR.from_stat(vst, filename=vn.vpath)) ret.sort(key=lambda x: x.filename) self.log("ls(%s): vfs-vols; |%d|" % (path, len(ret))) return ret _, vfs_ls, vfs_virt = vn.ls( rem, self.uname, not self.args.no_scandir, [[True, False], [False, True]], throw=True, ) ret = [SATTR.from_stat(x[1], filename=x[0]) for x in vfs_ls] for zs, vn2 in vfs_virt.items(): if not vn2.realpath: continue st = bos.stat(vn2.realpath) ret.append(SATTR.from_stat(st, filename=zs)) if self.uname not in vn.axs.udot: ret = [x for x in ret if not x.filename.split("/")[-1].startswith(".")] ret.sort(key=lambda x: x.filename) self.log("ls(%s): |%d|" % (path, len(ret))) return ret def stat(self, path: str) -> SATTR | int: try: return self._stat(path) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def lstat(self, path: str) -> SATTR | int: try: return self._stat(path) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _stat(self, vp: str) -> SATTR | int: vp = vp.strip("/") try: ap, vn, _ = self.v2a(vp) if ( self.uname not in vn.axs.uread and self.uname not in vn.axs.uwrite and self.uname not in vn.axs.uget ): self.log("stat(%s): EPERM" % (vp,)) return SFTP_PERMISSION_DENIED st = bos.stat(ap) self.log("stat(%s): %s" % (vp, st)) except: if vp not in self.vis: self.log("stat(%s): ENOENT" % (vp,)) return SFTP_NO_SUCH_FILE zi = int(time.time()) st = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi)) self.log("stat(%s): vfs-vols") return SATTR.from_stat(st) def open(self, path: str, flags: int, attr: SATTR) -> paramiko.SFTPHandle | int: try: return self._open(path, flags, attr) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _open(self, vp: str, iflag: int, attr: SATTR) -> paramiko.SFTPHandle | int: if ANYWIN: iflag |= os.O_BINARY if iflag & os.O_WRONLY: rd = False wr = True if iflag & os.O_APPEND: smode = "ab" else: smode = "wb" elif iflag & os.O_RDWR: rd = wr = True if iflag & os.O_APPEND: smode = "a+b" else: smode = "r+b" else: rd = True wr = False smode = "rb" try: vn, rem = self.asrv.vfs.get(vp, self.uname, rd, wr) ap = vn.canonical(rem, False) vf = vn.flags except Pebkac as ex: t = "denied open file [%s], iflag=%s, read=%s, write=%s: %s" self.log(t % (vp, iflag, rd, wr, ex)) return SFTP_PERMISSION_DENIED self.log("open(%s, %x, %s)" % (vp, iflag, smode)) if wr: try: st = bos.stat(ap) td = time.time() - st.st_mtime need_unlink = True except: need_unlink = False td = 0 xbu = vn.flags.get("xbu") if xbu: hr = runhook( self.log, None, self.hub.up2k, "xbu.sftp", xbu, ap, vp, "", "", "", 0, 0, "7.3.8.7", time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vp,) self.log(t, 3) return SFTP_PERMISSION_DENIED self.log("writing to [%s] => [%s]" % (vp, ap)) if wr and need_unlink: # type: ignore # !rm assert td # type: ignore # !rm if td >= -1 and td <= self.args.ftp_wt: # within permitted timeframe; allow overwrite or resume do_it = True elif self.args.no_del or self.args.ftp_no_ow: # file too old, or overwrite not allowed; reject do_it = False else: # allow overwrite if user has delete permission do_it = self.uname in vn.axs.udel if not do_it: t = "file already exists and no permission to overwrite: %s" self.log(t % (vp,)) return SFTP_PERMISSION_DENIED # Don't unlink file for append mode elif "a" not in smode: wunlink(self.log, ap, VF_CAREFUL) chmod = getattr(attr, "st_mode", None) if chmod is None: chmod = vf.get("chmod_f", 0o644) self.log("open(%s, %x): client did not chmod" % (vp, iflag)) else: self.log("open(%s, %x): client set chmod 0%o" % (vp, iflag, chmod)) try: fd = os.open(ap, iflag, chmod) except OSError as ex: t = "failed to os.open [%s] -> [%s] with iflag [%s] and chmod [%s]: %r" self.log(t % (vp, ap, iflag, chmod, ex), 3) return paramiko.SFTPServer.convert_errno(ex.errno) if iflag & os.O_CREAT: paramiko.SFTPServer.set_file_attr(ap, attr) try: f = os.fdopen(fd, smode) except OSError as ex: t = "failed to os.fdpen [%s] -> [%s] with smode [%s]: %r" self.log(t % (vp, ap, smode, ex), 3) return paramiko.SFTPServer.convert_errno(ex.errno) ret = SFTP_FH(iflag) ret.filename = ap ret.readfile = f if rd else None ret.writefile = f if wr else None return ret def remove(self, path: str) -> int: try: return self._remove(path) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _remove(self, vp: str) -> int: self.log("rm(%s)" % (vp,)) if self.args.no_del: self.log("The delete feature is disabled in server config") return SFTP_PERMISSION_DENIED try: self.hub.up2k.handle_rm(self.uname, self.ip, [vp], [], False, False) self.log("rm(%s): ok" % (vp,)) return SFTP_OK except Pebkac as ex: t = "denied delete [%s]: %s" self.log(t % (vp, ex)) if str(ex).startswith("file not found"): return SFTP_NO_SUCH_FILE try: # write-only client trying to rm before upload? ap, vn, _ = self.v2a(vp) if ( self.uname not in vn.axs.uread and self.uname not in vn.axs.uwrite and self.uname not in vn.axs.uget ): self.log("rm(%s): EPERM" % (vp,)) return SFTP_PERMISSION_DENIED if not bos.path.exists(ap): self.log(" `- file didn't exist; returning ENOENT") return SFTP_NO_SUCH_FILE except: pass return SFTP_PERMISSION_DENIED except OSError as ex: self.log("failed: rm(%s): %r" % (vp, ex)) return paramiko.SFTPServer.convert_errno(ex.errno) def rename(self, oldpath: str, newpath: str) -> int: try: return self._rename(oldpath, newpath) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _rename(self, svp: str, dvp: str) -> int: self.log("mv(%s, %s)" % (svp, dvp)) if self.args.no_mv: self.log("The rename/move feature is disabled in server config") svp = svp.strip("/") dvp = dvp.strip("/") try: self.hub.up2k.handle_mv("", self.uname, self.ip, svp, dvp) return SFTP_OK except Pebkac as ex: t = "denied rename [%s] to [%s]: %s" self.log(t % (svp, dvp, ex)) return SFTP_PERMISSION_DENIED except OSError as ex: self.log("mv(%s, %s): %r" % (svp, dvp, ex)) return paramiko.SFTPServer.convert_errno(ex.errno) def mkdir(self, path: str, attr: SATTR) -> int: try: return self._mkdir(path, attr) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _mkdir(self, vp: str, attr: SATTR) -> int: self.log("mkdir(%s)" % (vp,)) try: vn, rem = self.asrv.vfs.get(vp, self.uname, False, True) ap = os.path.join(vn.realpath, rem) bos.makedirs(ap, vf=vn.flags) # filezilla expects this if attr is not None: paramiko.SFTPServer.set_file_attr(ap, attr) return SFTP_OK except Pebkac as ex: t = "denied mkdir [%s]: %s" self.log(t % (vp, ex)) return SFTP_PERMISSION_DENIED except OSError as ex: self.log("mkdir(%s): %r" % (vp, ex)) return paramiko.SFTPServer.convert_errno(ex.errno) def rmdir(self, path: str) -> int: try: return self._rmdir(path) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _rmdir(self, vp: str) -> int: self.log("rmdir(%s)" % (vp,)) try: vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True) ap = os.path.join(vn.realpath, rem) bos.rmdir(ap) return SFTP_OK except Pebkac as ex: t = "denied rmdir [%s]: %s" self.log(t % (vp, ex)) return SFTP_PERMISSION_DENIED except OSError as ex: self.log("rmdir(%s): %r" % (vp, ex)) return paramiko.SFTPServer.convert_errno(ex.errno) def chattr(self, path: str, attr: SATTR) -> int: try: return self._chattr(path, attr) except: self.log("unhandled exception: %s" % (min_ex(),), 1) return SFTP_FAILURE def _chattr(self, vp: str, attr: SATTR) -> int: self.log("chattr(%s, %s)" % (vp, attr)) try: vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True) ap = os.path.join(vn.realpath, rem) paramiko.SFTPServer.set_file_attr(ap, attr) return SFTP_OK except Pebkac as ex: t = "denied chattr [%s]: %s" self.log(t % (vp, ex)) return SFTP_PERMISSION_DENIED except OSError as ex: self.log("chattr(%s): %r" % (vp, ex)) return paramiko.SFTPServer.convert_errno(ex.errno) def symlink(self, target_path: str, path: str) -> int: return SFTP_OP_UNSUPPORTED def readlink(self, path: str) -> str | int: return path def canonicalize(self, path: str) -> str: return "/%s" % (undot(path),) class Sftpd(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.args = args = hub.args self.log_func = hub.log self.srv: list[socket.socket] = [] self.bound: list[str] = [] self.sessions = {} ips = args.sftp_i if "::" in ips: ips.append("0.0.0.0") ips = [x for x in ips if not x.startswith(("unix:", "fd:"))] if args.sftp4: ips = [x for x in ips if ":" not in x] if not ips: self.log("cannot start sftp-server; no compatible IPs in -i", 1) return self.hostkeys = [] hostkeytypes = ( ("ed25519", "Ed25519Key", {}), # best ("ecdsa", "ECDSAKey", {"bits": 384}), ("rsa", "RSAKey", {"bits": 4096}), ("dsa", "DSSKey", {}), # worst ) for fname, aname, opts in hostkeytypes: fpath = "%s/ssh_host_%s_key" % (args.sftp_hostk, fname.lower()) try: pkey = getattr(paramiko, aname).from_private_key_file(fpath) except Exception as ex: try: genfun = getattr(paramiko, aname).generate except Exception as ex2: if args.sftpv or fname not in ("dsa", "ed25519"): # dsa dropped in 4.0 # ed25519 not supported yet self.log("cannot generate %s hostkey: %r" % (aname, ex2), 3) continue self.log("generating hostkey [%s] due to %r" % (fpath, ex)) pkey = genfun(**opts) pkey.write_private_key_file(fpath) pkey = getattr(paramiko, aname).from_private_key_file(fpath) self.hostkeys.append(pkey) if args.sftpv: self.log("loaded hostkey %r" % (pkey,)) ips = list(ODict.fromkeys(ips)) # dedup for ip in ips: self._bind(ip) self.log("listening @ %s port %s" % (self.bound, args.sftp)) def log(self, msg: str, c: Union[int, str] = 0) -> None: self.hub.log("sftp", msg, c) def _bind(self, ip: str) -> None: port = self.args.sftp try: ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET srv = socket.socket(ipv, socket.SOCK_STREAM) if not ANYWIN or self.args.reuseaddr: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) srv.settimeout(0) # == srv.setblocking(False) try: srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) except: pass # will create another ipv4 socket instead if getattr(self.args, "freebind", False): srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1) srv.bind((ip, port)) srv.listen(10) self.srv.append(srv) self.bound.append(ip) except Exception as ex: if ip == "0.0.0.0" and "::" in self.bound: try: srv.close() # type: ignore except: pass return # dualstack self.log("could not listen on (%s,%s): %r" % (ip, port, ex), 3) def _accept(self, srv: socket.socket) -> None: cli, addr = srv.accept() # cli.settimeout(0) # == srv.setblocking(False) self.log("%r is connecting" % (addr,)) zs = "sftp-%s" % (addr[0],) # Daemon(self._accept2, zs, (cli, addr)) self._accept2(cli, addr) def _accept2(self, cli, addr) -> None: tra = paramiko.Transport(cli) for hkey in self.hostkeys: tra.add_server_key(hkey) tra.set_subsystem_handler("sftp", paramiko.SFTPServer, SFTP_Srv) psrv = SSH_Srv(self.hub, addr) try: tra.start_server(server=psrv) except Exception as ex: self.log("%r could not establish connection: %r" % (addr, ex), 3) cli.close() return chan = tra.accept() if chan is None: self.log("%r did not open an sftp channel" % (addr,), 3) cli.close() return self.sessions[addr] = (chan, tra, psrv) # tra.join() # self.log("%r disconnected" % (addr,)) def run(self): lgr = logging.getLogger("paramiko.transport") lgr.setLevel(logging.DEBUG if self.args.sftpvv else logging.INFO) if self.args.no_poll: fun = self._run_select else: fun = self._run_poll Daemon(fun, "sftpd") def _run_select(self): while not self.hub.stopping: rx, _, _ = select.select(self.srv, [], [], 180) for sck in rx: self._accept(sck) def _run_poll(self): fd2sck = {} poll = select.poll() for sck in self.srv: fd = sck.fileno() fd2sck[fd] = sck poll.register(fd, select.POLLIN) while not self.hub.stopping: pr = poll.poll(180 * 1000) rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN] for sck in rx: self._accept(sck) ================================================ FILE: copyparty/smbd.py ================================================ # coding: utf-8 import inspect import logging import os import random import stat import sys import time from types import SimpleNamespace from .__init__ import ANYWIN, EXE, TYPE_CHECKING from .authsrv import LEELOO_DALLAS, VFS from .bos import bos from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin if True: # pylint: disable=using-constant-test from typing import Any, Union if TYPE_CHECKING: from .svchub import SvcHub lg = logging.getLogger("smb") debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error) class SMB(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.args = hub.args self.asrv = hub.asrv self.log = hub.log self.files: dict[int, tuple[float, str]] = {} self.noacc = self.args.smba self.accs = not self.args.smba lg.setLevel(logging.DEBUG if self.args.smbvvv else logging.INFO) for x in ["impacket", "impacket.smbserver"]: lgr = logging.getLogger(x) lgr.setLevel(logging.DEBUG if self.args.smbvv else logging.INFO) try: from impacket import smbserver from impacket.ntlm import compute_lmhash, compute_nthash except ImportError: if EXE: print("copyparty.exe cannot do SMB") sys.exit(1) m = "\033[36m\n{}\033[31m\n\nERROR: need 'impacket'; please run this command:\033[33m\n {} -m pip install --user impacket\n\033[0m" print(m.format(min_ex(), pybin)) sys.exit(1) # patch vfs into smbserver.os fos = SimpleNamespace() for k in os.__dict__: try: setattr(fos, k, getattr(os, k)) except: pass fos.close = self._close fos.listdir = self._listdir fos.mkdir = self._mkdir fos.open = self._open fos.remove = self._unlink fos.rename = self._rename fos.stat = self._stat fos.unlink = self._unlink fos.utime = self._utime smbserver.os = fos # ...and smbserver.os.path fop = SimpleNamespace() for k in os.path.__dict__: try: setattr(fop, k, getattr(os.path, k)) except: pass fop.exists = self._p_exists fop.getsize = self._p_getsize fop.isdir = self._p_isdir smbserver.os.path = fop if not self.args.smb_nwa_2: fop.join = self._p_join # other patches smbserver.isInFileJail = self._is_in_file_jail self._disarm() ip = next((x for x in self.args.smb_i if ":" not in x), None) if not ip: self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3) ip = "0.0.0.0" port = int(self.args.smb_port) srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port) try: if self.accs: srv.setAuthCallback(self._auth_cb) except: self.accs = False self.noacc = True t = "impacket too old; access permissions will not work! all accounts are admin!" self.log("smb", t, 1) ro = "no" if self.args.smbw else "yes" # (does nothing) srv.addShare("A", "/", readOnly=ro) srv.setSMB2Support(not self.args.smb1) for name, pwd in self.asrv.acct.items(): for u, p in ((name, pwd), (pwd, "k")): lmhash = compute_lmhash(p) nthash = compute_nthash(p) srv.addCredential(u, 0, lmhash, nthash) chi = [random.randint(0, 255) for x in range(8)] cha = "".join(["{:02x}".format(x) for x in chi]) srv.setSMBChallenge(cha) self.srv = srv self.stop = srv.stop self.log("smb", "listening @ {}:{}".format(ip, port)) def nlog(self, msg: str, c: Union[int, str] = 0) -> None: self.log("smb", msg, c) def start(self) -> None: Daemon(self.srv.start, "smbd") def _auth_cb(self, *a, **ka): debug("auth-result: %s %s", a, ka) conndata = ka["connData"] auth_ok = conndata["Authenticated"] uname = ka["user_name"] if auth_ok else "*" uname = self.asrv.iacct.get(uname, uname) or "*" oldname = conndata.get("partygoer", "*") or "*" cli_ip = conndata["ClientIP"] cli_hn = ka["host_name"] if uname != "*": conndata["partygoer"] = uname info("client %s [%s] authed as %s", cli_ip, cli_hn, uname) elif oldname != "*": info("client %s [%s] keeping old auth as %s", cli_ip, cli_hn, oldname) elif auth_ok: info("client %s [%s] authed as [*] (anon)", cli_ip, cli_hn) else: info("client %s [%s] rejected", cli_ip, cli_hn) def _uname(self) -> str: if self.noacc: return LEELOO_DALLAS if not self.asrv.acct: return "*" try: # you found it! my single worst bit of code so far # (if you can think of a better way to track users through impacket i'm all ears) cf0 = inspect.currentframe().f_back.f_back cf = cf0.f_back for n in range(3): cl = cf.f_locals if "connData" in cl: return cl["connData"]["partygoer"] cf = cf.f_back raise Exception() except: warning( "nyoron... %s <<-- %s <<-- %s <<-- %s", cf0.f_code.co_name, cf0.f_back.f_code.co_name, cf0.f_back.f_back.f_code.co_name, cf0.f_back.f_back.f_back.f_code.co_name, ) return "*" def _v2a( self, caller: str, vpath: str, *a: Any, uname="", perms=None ) -> tuple[VFS, str]: vpath = vpath.replace("\\", "/").lstrip("/") # cf = inspect.currentframe().f_back # c1 = cf.f_back.f_code.co_name # c2 = cf.f_code.co_name if not uname: uname = self._uname() if not perms: perms = [True, True] debug('%s("%s", %s) %s @%s\033[K\033[0m', caller, vpath, str(a), perms, uname) vfs, rem = self.asrv.vfs.get(vpath, uname, *perms) if not vfs.realpath: raise Exception("unmapped vfs") return vfs, vfs.canonical(rem, False) def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]: vpath = vpath.replace("\\", "/").lstrip("/") # caller = inspect.currentframe().f_back.f_code.co_name uname = self._uname() # debug('listdir("%s", %s) @%s\033[K\033[0m', vpath, str(a), uname) vfs, rem = self.asrv.vfs.get(vpath, uname, False, False) if not vfs.realpath: raise Exception("unmapped vfs") _, vfs_ls, vfs_virt = vfs.ls( rem, uname, not self.args.no_scandir, [[False, False]] ) dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] fils = [x[0] for x in vfs_ls if x[0] not in dirs] ls = list(vfs_virt.keys()) + dirs + fils if self.args.smb_nwa_1: return ls # clients crash somewhere around 65760 byte ret = [] sz = 112 * 2 # ['.', '..'] for n, fn in enumerate(ls): if sz >= 64000: t = "listing only %d of %d files (%d byte) in /%s for performance; see --smb-nwa-1" warning(t, n, len(ls), sz, vpath) break nsz = len(fn.encode("utf-16", "replace")) nsz = ((nsz + 7) // 8) * 8 sz += 104 + nsz ret.append(fn) return ret def _open( self, vpath: str, flags: int, *a: Any, chmod: int = 0o777, **ka: Any ) -> Any: f_ro = os.O_RDONLY if ANYWIN: f_ro |= os.O_BINARY wr = flags != f_ro if wr and not self.args.smbw: yeet("blocked write (no --smbw): " + vpath) uname = self._uname() vfs, ap = self._v2a("open", vpath, *a, uname=uname, perms=[True, wr]) if wr: if not vfs.axs.uwrite: t = "blocked write (no-write-acc %s): /%s @%s" yeet(t % (vfs.axs.uwrite, vpath, uname)) ap = absreal(ap) xbu = vfs.flags.get("xbu") if xbu: hr = runhook( self.nlog, None, self.hub.up2k, "xbu.smb", xbu, ap, vpath, "", "", "", 0, 0, "1.7.6.2", time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "blocked by xbu server config: %r" % (vpath,) yeet(t) ret = bos.open(ap, flags, *a, mode=chmod, **ka) if wr: now = time.time() nf = len(self.files) if nf > 9000: oldest = min([x[0] for x in self.files.values()]) cutoff = oldest + (now - oldest) / 2 self.files = {k: v for k, v in self.files.items() if v[0] > cutoff} info("was tracking %d files, now %d", nf, len(self.files)) vpath = vpath.replace("\\", "/").lstrip("/") self.files[ret] = (now, vpath) return ret def _close(self, fd: int) -> None: os.close(fd) if fd not in self.files: return _, vp = self.files.pop(fd) vp, fn = os.path.split(vp) vfs, rem = self.hub.asrv.vfs.get(vp, self._uname(), False, True) vfs, rem = vfs.get_dbv(rem) self.hub.up2k.hash_file( vfs.realpath, vfs.vpath, vfs.flags, rem, fn, "1.7.6.2", time.time(), "", ) def _rename(self, vp1: str, vp2: str) -> None: if not self.args.smbw: yeet("blocked rename (no --smbw): " + vp1) vp1 = vp1.lstrip("/") vp2 = vp2.lstrip("/") uname = self._uname() vfs2, ap2 = self._v2a("rename", vp2, vp1, uname=uname) if not vfs2.axs.uwrite: t = "blocked write (no-write-acc %s): /%s @%s" yeet(t % (vfs2.axs.uwrite, vp2, uname)) vfs1, _ = self.asrv.vfs.get(vp1, uname, True, True, True) if not vfs1.axs.umove: t = "blocked rename (no-move-acc %s): /%s @%s" yeet(t % (vfs1.axs.umove, vp1, uname)) self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2) try: bos.makedirs(ap2, vf=vfs2.flags) except: pass def _mkdir(self, vpath: str) -> None: if not self.args.smbw: yeet("blocked mkdir (no --smbw): " + vpath) uname = self._uname() vfs, ap = self._v2a("mkdir", vpath, uname=uname) if not vfs.axs.uwrite: t = "blocked mkdir (no-write-acc %s): /%s @%s" yeet(t % (vfs.axs.uwrite, vpath, uname)) return bos.mkdir(ap, vfs.flags["chmod_d"]) def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result: try: ap = self._v2a("stat", vpath, *a, perms=[True, False])[1] ret = bos.stat(ap, *a, **ka) # debug(" `-stat:ok") return ret except: # white lie: windows freaks out if we raise due to an offline volume # debug(" `-stat:NOPE (faking a directory)") ts = int(time.time()) return os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, ts, ts, ts)) def _unlink(self, vpath: str) -> None: if not self.args.smbw: yeet("blocked delete (no --smbw): " + vpath) # return bos.unlink(self._v2a("stat", vpath, *a)[1]) uname = self._uname() vfs, ap = self._v2a( "delete", vpath, uname=uname, perms=[True, False, False, True] ) if not vfs.axs.udel: yeet("blocked delete (no-del-acc): " + vpath) vpath = vpath.replace("\\", "/").lstrip("/") self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False, False) def _utime(self, vpath: str, times: tuple[float, float]) -> None: if not self.args.smbw: yeet("blocked utime (no --smbw): " + vpath) uname = self._uname() vfs, ap = self._v2a("utime", vpath, uname=uname) if not vfs.axs.uwrite: t = "blocked utime (no-write-acc %s): /%s @%s" yeet(t % (vfs.axs.uwrite, vpath, uname)) bos.utime_c(info, ap, int(times[1]), False) def _p_exists(self, vpath: str) -> bool: # ap = "?" try: ap = self._v2a("p.exists", vpath, perms=[True, False])[1] bos.stat(ap) # debug(" `-exists((%s)->(%s)):ok", vpath, ap) return True except: # debug(" `-exists((%s)->(%s)):NOPE", vpath, ap) return False def _p_getsize(self, vpath: str) -> int: st = bos.stat(self._v2a("p.getsize", vpath, perms=[True, False])[1]) return st.st_size def _p_isdir(self, vpath: str) -> bool: try: st = bos.stat(self._v2a("p.isdir", vpath, perms=[True, False])[1]) ret = stat.S_ISDIR(st.st_mode) # debug(" `-isdir:%s:%s", st.st_mode, ret) return ret except: return False def _p_join(self, *a) -> str: # impacket.smbserver reads globs from queryDirectoryRequest['Buffer'] # where somehow `fds.*` becomes `fds"*` so lets fix that ret = os.path.join(*a) return ret.replace('"', ".") # type: ignore def _hook(self, *a: Any, **ka: Any) -> None: src = inspect.currentframe().f_back.f_code.co_name error("\033[31m%s:hook(%s)\033[0m", src, a) raise Exception("nope") def _disarm(self) -> None: from impacket import smbserver smbserver.os.chmod = self._hook smbserver.os.chown = self._hook smbserver.os.ftruncate = self._hook smbserver.os.lchown = self._hook smbserver.os.link = self._hook smbserver.os.lstat = self._hook smbserver.os.replace = self._hook smbserver.os.scandir = self._hook smbserver.os.symlink = self._hook smbserver.os.truncate = self._hook smbserver.os.walk = self._hook smbserver.os.path.abspath = self._hook smbserver.os.path.expanduser = self._hook smbserver.os.path.expandvars = self._hook smbserver.os.path.getatime = self._hook smbserver.os.path.getctime = self._hook smbserver.os.path.getmtime = self._hook smbserver.os.path.isabs = self._hook smbserver.os.path.isfile = self._hook smbserver.os.path.islink = self._hook smbserver.os.path.realpath = self._hook def _is_in_file_jail(self, *a: Any) -> bool: # handled by vfs return True def yeet(msg: str) -> None: info(msg) raise Exception(msg) ================================================ FILE: copyparty/ssdp.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import errno import re import select import socket import time from .__init__ import TYPE_CHECKING from .multicast import MC_Sck, MCast from .util import CachedSet, formatdate, html_escape, min_ex if TYPE_CHECKING: from .broker_util import BrokerCli from .httpcli import HttpCli from .svchub import SvcHub if True: # pylint: disable=using-constant-test from typing import Optional, Union GRP = "239.255.255.250" class SSDP_Sck(MC_Sck): def __init__(self, *a): super(SSDP_Sck, self).__init__(*a) self.hport = 0 class SSDPr(object): """generates http responses for httpcli""" def __init__(self, broker: "BrokerCli") -> None: self.broker = broker self.args = broker.args def reply(self, hc: "HttpCli") -> bool: if hc.vpath.endswith("device.xml"): return self.tx_device(hc) hc.reply(b"unknown request", 400) return False def tx_device(self, hc: "HttpCli") -> bool: zs = """ 1 0 {} {} urn:schemas-upnp-org:device:Basic:1 {} file server ed https://ocv.me/ copyparty https://github.com/9001/copyparty/ {} urn:schemas-upnp-org:device:Basic:1 urn:schemas-upnp-org:device:Basic /.cpr/ssdp/services.xml /.cpr/ssdp/services.xml /.cpr/ssdp/services.xml """ c = html_escape sip, sport = hc.s.getsockname()[:2] sip = sip.replace("::ffff:", "") proto = "https" if self.args.https_only else "http" ubase = "{}://{}:{}".format(proto, sip, sport) zsl = self.args.zsl url = zsl if "://" in zsl else ubase + "/" + zsl.lstrip("/") name = self.args.doctitle zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid)) hc.reply(zs.encode("utf-8", "replace")) return False # close connection class SSDPd(MCast): """communicates with ssdp clients over multicast""" def __init__(self, hub: "SvcHub", ngen: int) -> None: al = hub.args vinit = al.zsv and not al.zmv super(SSDPd, self).__init__( hub, SSDP_Sck, al.zs_on, al.zs_off, GRP, "", 1900, vinit ) self.srv: dict[socket.socket, SSDP_Sck] = {} self.logsrc = "SSDP-{}".format(ngen) self.ngen = ngen self.rxc = CachedSet(0.7) self.txc = CachedSet(5) # win10: every 3 sec self.ptn_st = re.compile(b"\nst: *upnp:rootdevice", re.I) def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func(self.logsrc, msg, c) def run(self) -> None: try: bound = self.create_servers() except: t = "no server IP matches the ssdp config\n{}" self.log(t.format(min_ex()), 1) bound = [] if not bound: self.log("failed to announce copyparty services on the network", 3) return # find http port for this listening ip for srv in self.srv.values(): tcps = self.hub.tcpsrv.bound hp = next((x[1] for x in tcps if x[0] in ("0.0.0.0", srv.ip)), 0) hp = hp or next((x[1] for x in tcps if x[0] == "::"), 0) if not hp: hp = tcps[0][1] self.log("assuming port {} for {}".format(hp, srv.ip), 3) srv.hport = hp self.log("listening") try: self.run2() except OSError as ex: if ex.errno != errno.EBADF: raise self.log("stopping due to {}".format(ex), "90") self.log("stopped", 2) def run2(self) -> None: try: if self.args.no_poll: raise Exception() fd2sck = {} srvpoll = select.poll() for sck in self.srv: fd = sck.fileno() fd2sck[fd] = sck srvpoll.register(fd, select.POLLIN) except Exception as ex: srvpoll = None if not self.args.no_poll: t = "WARNING: failed to poll(), will use select() instead: %r" self.log(t % (ex,), 3) while self.running: if srvpoll: pr = srvpoll.poll((self.args.z_chk or 180) * 1000) rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN] else: rdy = select.select(self.srv, [], [], self.args.z_chk or 180) rx: list[socket.socket] = rdy[0] # type: ignore self.rxc.cln() buf = b"" addr = ("0", 0) for sck in rx: try: buf, addr = sck.recvfrom(4096) self.eat(buf, addr) except: if not self.running: break t = "{} {} \033[33m|{}| {}\n{}".format( self.srv[sck].name, addr, len(buf), repr(buf)[2:-1], min_ex() ) self.log(t, 6) def stop(self) -> None: self.running = False for srv in self.srv.values(): try: srv.sck.close() except: pass self.srv.clear() def eat(self, buf: bytes, addr: tuple[str, int]) -> None: cip = addr[0] if cip.startswith("169.254") and not self.ll_ok: return if buf in self.rxc.c: return srv: Optional[SSDP_Sck] = self.map_client(cip) # type: ignore if not srv: return self.rxc.add(buf) if not buf.startswith(b"M-SEARCH * HTTP/1."): return if not self.ptn_st.search(buf): return if self.args.zsv: t = "{} [{}] \033[36m{} \033[0m|{}|" self.log(t.format(srv.name, srv.ip, cip, len(buf)), "90") zs = """ HTTP/1.1 200 OK CACHE-CONTROL: max-age=1800 DATE: {0} EXT: LOCATION: http://{1}:{2}/.cpr/ssdp/device.xml OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 01-NLS: {3} SERVER: UPnP/1.0 ST: upnp:rootdevice USN: {3}::upnp:rootdevice BOOTID.UPNP.ORG: 0 CONFIGID.UPNP.ORG: 1 """ v4 = srv.ip.replace("::ffff:", "") zs = zs.format(formatdate(), v4, srv.hport, self.args.zsid) zb = zs[1:].replace("\n", "\r\n").encode("utf-8", "replace") srv.sck.sendto(zb, addr[:2]) if cip not in self.txc.c: self.log("{} [{}] --> {}".format(srv.name, srv.ip, cip), 6) self.txc.add(cip) self.txc.cln() ================================================ FILE: copyparty/star.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import re import stat import tarfile from queue import Queue from .authsrv import AuthSrv from .bos import bos from .sutil import StreamArc, errdesc from .util import Daemon, fsenc, min_ex if True: # pylint: disable=using-constant-test from typing import Any, Generator, Optional from .util import NamedLogger class QFile(object): # inherit io.StringIO for painful typing """file-like object which buffers writes into a queue""" def __init__(self) -> None: self.q: Queue[Optional[bytes]] = Queue(64) self.bq: list[bytes] = [] self.nq = 0 def write(self, buf: Optional[bytes]) -> None: if buf is None or self.nq >= 240 * 1024: self.q.put(b"".join(self.bq)) self.bq = [] self.nq = 0 if buf is None: self.q.put(None) else: self.bq.append(buf) self.nq += len(buf) class StreamTar(StreamArc): """construct in-memory tar file from the given path""" def __init__( self, log: "NamedLogger", asrv: AuthSrv, fgen: Generator[dict[str, Any], None, None], cmp: str = "", **kwargs: Any ): super(StreamTar, self).__init__(log, asrv, fgen) self.ci = 0 self.co = 0 self.qfile = QFile() self.errf: dict[str, Any] = {} # python 3.8 changed to PAX_FORMAT as default; # slower, bigger, and no particular advantage fmt = tarfile.GNU_FORMAT if "pax" in cmp: # unless a client asks for it (currently # gnu-tar has wider support than pax-tar) fmt = tarfile.PAX_FORMAT cmp = re.sub(r"[^a-z0-9]*pax[^a-z0-9]*", "", cmp) try: cmp, zs = cmp.replace(":", ",").split(",") lv = int(zs) except: lv = -1 arg = {"name": None, "fileobj": self.qfile, "mode": "w", "format": fmt} if cmp == "gz": fun = tarfile.TarFile.gzopen arg["compresslevel"] = lv if lv >= 0 else 3 elif cmp == "bz2": fun = tarfile.TarFile.bz2open arg["compresslevel"] = lv if lv >= 0 else 2 elif cmp == "xz": fun = tarfile.TarFile.xzopen arg["preset"] = lv if lv >= 0 else 1 else: fun = tarfile.open arg["mode"] = "w|" self.tar = fun(**arg) Daemon(self._gen, "star-gen") def gen(self) -> Generator[Optional[bytes], None, None]: buf = b"" try: while True: buf = self.qfile.q.get() if not buf: break self.co += len(buf) yield buf yield None finally: while buf: try: buf = self.qfile.q.get() except: pass if self.errf: bos.unlink(self.errf["ap"]) def ser(self, f: dict[str, Any]) -> None: name = f["vp"] src = f["ap"] fsi = f["st"] if stat.S_ISDIR(fsi.st_mode): return inf = tarfile.TarInfo(name=name) inf.mode = fsi.st_mode inf.size = fsi.st_size inf.mtime = fsi.st_mtime inf.uid = 0 inf.gid = 0 self.ci += inf.size with open(fsenc(src), "rb", self.args.iobuf) as fo: self.tar.addfile(inf, fo) def _gen(self) -> None: errors = [] for f in self.fgen: if "err" in f: errors.append((f["vp"], f["err"])) continue if self.stopped: break try: self.ser(f) except: ex = min_ex(5, True).replace("\n", "\n-- ") errors.append((f["vp"], ex)) if errors: self.errf, txt = errdesc(self.asrv.vfs, errors) self.log("\n".join(([repr(self.errf)] + txt[1:]))) self.ser(self.errf) self.tar.close() self.qfile.write(None) ================================================ FILE: copyparty/stolen/__init__.py ================================================ ================================================ FILE: copyparty/stolen/dnslib/README.md ================================================ `dnslib` but heavily simplified/feature-stripped L: MIT Copyright (c) 2010 - 2017 Paul Chakravarti https://github.com/paulc/dnslib/ ================================================ FILE: copyparty/stolen/dnslib/__init__.py ================================================ # coding: utf-8 """ L: MIT Copyright (c) 2010 - 2017 Paul Chakravarti https://github.com/paulc/dnslib/tree/0.9.23 """ from .dns import * version = "0.9.23" ================================================ FILE: copyparty/stolen/dnslib/bimap.py ================================================ # coding: utf-8 import types class BimapError(Exception): pass class Bimap(object): def __init__(self, name, forward, error=AttributeError): self.name = name self.error = error self.forward = forward.copy() self.reverse = dict([(v, k) for (k, v) in list(forward.items())]) def get(self, k, default=None): try: return self.forward[k] except KeyError: return default or str(k) def __getitem__(self, k): try: return self.forward[k] except KeyError: if isinstance(self.error, types.FunctionType): return self.error(self.name, k, True) else: raise self.error("%s: Invalid forward lookup: [%s]" % (self.name, k)) def __getattr__(self, k): try: if k == "__wrapped__": raise AttributeError() return self.reverse[k] except KeyError: if isinstance(self.error, types.FunctionType): return self.error(self.name, k, False) else: raise self.error("%s: Invalid reverse lookup: [%s]" % (self.name, k)) ================================================ FILE: copyparty/stolen/dnslib/bit.py ================================================ # coding: utf-8 from __future__ import print_function def get_bits(data, offset, bits=1): mask = ((1 << bits) - 1) << offset return (data & mask) >> offset def set_bits(data, value, offset, bits=1): mask = ((1 << bits) - 1) << offset clear = 0xFFFF ^ mask data = (data & clear) | ((value << offset) & mask) return data ================================================ FILE: copyparty/stolen/dnslib/buffer.py ================================================ # coding: utf-8 import binascii import struct class BufferError(Exception): pass class Buffer(object): def __init__(self, data=b""): self.data = bytearray(data) self.offset = 0 def remaining(self): return len(self.data) - self.offset def get(self, length): if length > self.remaining(): raise BufferError( "Not enough bytes [offset=%d,remaining=%d,requested=%d]" % (self.offset, self.remaining(), length) ) start = self.offset end = self.offset + length self.offset += length return bytes(self.data[start:end]) def hex(self): return binascii.hexlify(self.data) def pack(self, fmt, *args): self.offset += struct.calcsize(fmt) self.data += struct.pack(fmt, *args) def append(self, s): self.offset += len(s) self.data += s def update(self, ptr, fmt, *args): s = struct.pack(fmt, *args) self.data[ptr : ptr + len(s)] = s def unpack(self, fmt): try: data = self.get(struct.calcsize(fmt)) return struct.unpack(fmt, data) except struct.error: raise BufferError( "Error unpacking struct '%s' <%s>" % (fmt, binascii.hexlify(data).decode()) ) def __len__(self): return len(self.data) ================================================ FILE: copyparty/stolen/dnslib/dns.py ================================================ # coding: utf-8 from __future__ import print_function import binascii from itertools import chain from .bimap import Bimap, BimapError from .bit import get_bits, set_bits from .buffer import BufferError from .label import DNSBuffer, DNSLabel, set_avahi_379 from .ranges import IP4, IP6, H, I, check_bytes try: range = xrange except: pass class DNSError(Exception): pass def unknown_qtype(name, key, forward): if forward: try: return "TYPE%d" % (key,) except: raise DNSError("%s: Invalid forward lookup: [%s]" % (name, key)) else: if key.startswith("TYPE"): try: return int(key[4:]) except: pass raise DNSError("%s: Invalid reverse lookup: [%s]" % (name, key)) QTYPE = Bimap( "QTYPE", {1: "A", 12: "PTR", 16: "TXT", 28: "AAAA", 33: "SRV", 47: "NSEC", 255: "ANY"}, unknown_qtype, ) CLASS = Bimap("CLASS", {1: "IN", 254: "None", 255: "*", 0x8001: "F_IN"}, DNSError) QR = Bimap("QR", {0: "QUERY", 1: "RESPONSE"}, DNSError) RCODE = Bimap( "RCODE", { 0: "NOERROR", 1: "FORMERR", 2: "SERVFAIL", 3: "NXDOMAIN", 4: "NOTIMP", 5: "REFUSED", 6: "YXDOMAIN", 7: "YXRRSET", 8: "NXRRSET", 9: "NOTAUTH", 10: "NOTZONE", }, DNSError, ) OPCODE = Bimap( "OPCODE", {0: "QUERY", 1: "IQUERY", 2: "STATUS", 4: "NOTIFY", 5: "UPDATE"}, DNSError ) def label(label, origin=None): if label.endswith("."): return DNSLabel(label) else: return (origin if isinstance(origin, DNSLabel) else DNSLabel(origin)).add(label) class DNSRecord(object): @classmethod def parse(cls, packet) -> "DNSRecord": buffer = DNSBuffer(packet) try: header = DNSHeader.parse(buffer) questions = [] rr = [] auth = [] ar = [] for i in range(header.q): questions.append(DNSQuestion.parse(buffer)) for i in range(header.a): rr.append(RR.parse(buffer)) for i in range(header.auth): auth.append(RR.parse(buffer)) for i in range(header.ar): ar.append(RR.parse(buffer)) return cls(header, questions, rr, auth=auth, ar=ar) except (BufferError, BimapError) as e: raise DNSError( "Error unpacking DNSRecord [offset=%d]: %s" % (buffer.offset, e) ) @classmethod def question(cls, qname, qtype="A", qclass="IN"): return DNSRecord( q=DNSQuestion(qname, getattr(QTYPE, qtype), getattr(CLASS, qclass)) ) def __init__( self, header=None, questions=None, rr=None, q=None, a=None, auth=None, ar=None ) -> None: self.header = header or DNSHeader() self.questions: list[DNSQuestion] = questions or [] self.rr: list[RR] = rr or [] self.auth: list[RR] = auth or [] self.ar: list[RR] = ar or [] if q: self.questions.append(q) if a: self.rr.append(a) self.set_header_qa() def reply(self, ra=1, aa=1): return DNSRecord( DNSHeader(id=self.header.id, bitmap=self.header.bitmap, qr=1, ra=ra, aa=aa), q=self.q, ) def add_question(self, *q) -> None: self.questions.extend(q) self.set_header_qa() def add_answer(self, *rr) -> None: self.rr.extend(rr) self.set_header_qa() def add_auth(self, *auth) -> None: self.auth.extend(auth) self.set_header_qa() def add_ar(self, *ar) -> None: self.ar.extend(ar) self.set_header_qa() def set_header_qa(self) -> None: self.header.q = len(self.questions) self.header.a = len(self.rr) self.header.auth = len(self.auth) self.header.ar = len(self.ar) def get_q(self): return self.questions[0] if self.questions else DNSQuestion() q = property(get_q) def get_a(self): return self.rr[0] if self.rr else RR() a = property(get_a) def pack(self) -> bytes: self.set_header_qa() buffer = DNSBuffer() self.header.pack(buffer) for q in self.questions: q.pack(buffer) for rr in self.rr: rr.pack(buffer) for auth in self.auth: auth.pack(buffer) for ar in self.ar: ar.pack(buffer) return buffer.data def truncate(self): return DNSRecord(DNSHeader(id=self.header.id, bitmap=self.header.bitmap, tc=1)) def format(self, prefix="", sort=False): s = sorted if sort else lambda x: x sections = [repr(self.header)] sections.extend(s([repr(q) for q in self.questions])) sections.extend(s([repr(rr) for rr in self.rr])) sections.extend(s([repr(rr) for rr in self.auth])) sections.extend(s([repr(rr) for rr in self.ar])) return prefix + ("\n" + prefix).join(sections) short = format def __repr__(self): return self.format() __str__ = __repr__ class DNSHeader(object): id = H("id") bitmap = H("bitmap") q = H("q") a = H("a") auth = H("auth") ar = H("ar") @classmethod def parse(cls, buffer): try: (id, bitmap, q, a, auth, ar) = buffer.unpack("!HHHHHH") return cls(id, bitmap, q, a, auth, ar) except (BufferError, BimapError) as e: raise DNSError( "Error unpacking DNSHeader [offset=%d]: %s" % (buffer.offset, e) ) def __init__(self, id=None, bitmap=None, q=0, a=0, auth=0, ar=0, **args) -> None: self.id = id if id else 0 if bitmap is None: self.bitmap = 0 else: self.bitmap = bitmap self.q = q self.a = a self.auth = auth self.ar = ar for k, v in args.items(): if k.lower() == "qr": self.qr = v elif k.lower() == "opcode": self.opcode = v elif k.lower() == "aa": self.aa = v elif k.lower() == "tc": self.tc = v elif k.lower() == "rd": self.rd = v elif k.lower() == "ra": self.ra = v elif k.lower() == "z": self.z = v elif k.lower() == "ad": self.ad = v elif k.lower() == "cd": self.cd = v elif k.lower() == "rcode": self.rcode = v def get_qr(self): return get_bits(self.bitmap, 15) def set_qr(self, val): self.bitmap = set_bits(self.bitmap, val, 15) qr = property(get_qr, set_qr) def get_opcode(self): return get_bits(self.bitmap, 11, 4) def set_opcode(self, val): self.bitmap = set_bits(self.bitmap, val, 11, 4) opcode = property(get_opcode, set_opcode) def get_aa(self): return get_bits(self.bitmap, 10) def set_aa(self, val): self.bitmap = set_bits(self.bitmap, val, 10) aa = property(get_aa, set_aa) def get_tc(self): return get_bits(self.bitmap, 9) def set_tc(self, val): self.bitmap = set_bits(self.bitmap, val, 9) tc = property(get_tc, set_tc) def get_rd(self): return get_bits(self.bitmap, 8) def set_rd(self, val): self.bitmap = set_bits(self.bitmap, val, 8) rd = property(get_rd, set_rd) def get_ra(self): return get_bits(self.bitmap, 7) def set_ra(self, val): self.bitmap = set_bits(self.bitmap, val, 7) ra = property(get_ra, set_ra) def get_z(self): return get_bits(self.bitmap, 6) def set_z(self, val): self.bitmap = set_bits(self.bitmap, val, 6) z = property(get_z, set_z) def get_ad(self): return get_bits(self.bitmap, 5) def set_ad(self, val): self.bitmap = set_bits(self.bitmap, val, 5) ad = property(get_ad, set_ad) def get_cd(self): return get_bits(self.bitmap, 4) def set_cd(self, val): self.bitmap = set_bits(self.bitmap, val, 4) cd = property(get_cd, set_cd) def get_rcode(self): return get_bits(self.bitmap, 0, 4) def set_rcode(self, val): self.bitmap = set_bits(self.bitmap, val, 0, 4) rcode = property(get_rcode, set_rcode) def pack(self, buffer): buffer.pack("!HHHHHH", self.id, self.bitmap, self.q, self.a, self.auth, self.ar) def __repr__(self): f = [ self.aa and "AA", self.tc and "TC", self.rd and "RD", self.ra and "RA", self.z and "Z", self.ad and "AD", self.cd and "CD", ] if OPCODE.get(self.opcode) == "UPDATE": f1 = "zo" f2 = "pr" f3 = "up" f4 = "ad" else: f1 = "q" f2 = "a" f3 = "ns" f4 = "ar" return ( "" % ( self.id, QR.get(self.qr), OPCODE.get(self.opcode), ",".join(filter(None, f)), RCODE.get(self.rcode), f1, self.q, f2, self.a, f3, self.auth, f4, self.ar, ) ) __str__ = __repr__ class DNSQuestion(object): @classmethod def parse(cls, buffer): try: qname = buffer.decode_name() qtype, qclass = buffer.unpack("!HH") return cls(qname, qtype, qclass) except (BufferError, BimapError) as e: raise DNSError( "Error unpacking DNSQuestion [offset=%d]: %s" % (buffer.offset, e) ) def __init__(self, qname=None, qtype=1, qclass=1) -> None: self.qname = qname self.qtype = qtype self.qclass = qclass def set_qname(self, qname): if isinstance(qname, DNSLabel): self._qname = qname else: self._qname = DNSLabel(qname) def get_qname(self): return self._qname qname = property(get_qname, set_qname) def pack(self, buffer): buffer.encode_name(self.qname) buffer.pack("!HH", self.qtype, self.qclass) def __repr__(self): return "" % ( self.qname, QTYPE.get(self.qtype), CLASS.get(self.qclass), ) __str__ = __repr__ class RR(object): rtype = H("rtype") rclass = H("rclass") ttl = I("ttl") rdlength = H("rdlength") @classmethod def parse(cls, buffer): try: rname = buffer.decode_name() rtype, rclass, ttl, rdlength = buffer.unpack("!HHIH") if rdlength: rdata = RDMAP.get(QTYPE.get(rtype), RD).parse(buffer, rdlength) else: rdata = RD(b"a") return cls(rname, rtype, rclass, ttl, rdata) except (BufferError, BimapError) as e: raise DNSError("Error unpacking RR [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, rname=None, rtype=1, rclass=1, ttl=0, rdata=None) -> None: self.rname = rname self.rtype = rtype self.rclass = rclass self.ttl = ttl self.rdata = rdata def set_rname(self, rname): if isinstance(rname, DNSLabel): self._rname = rname else: self._rname = DNSLabel(rname) def get_rname(self): return self._rname rname = property(get_rname, set_rname) def pack(self, buffer): buffer.encode_name(self.rname) buffer.pack("!HHI", self.rtype, self.rclass, self.ttl) rdlength_ptr = buffer.offset buffer.pack("!H", 0) start = buffer.offset self.rdata.pack(buffer) end = buffer.offset buffer.update(rdlength_ptr, "!H", end - start) def __repr__(self): return "" % ( self.rname, QTYPE.get(self.rtype), CLASS.get(self.rclass), self.ttl, self.rdata, ) __str__ = __repr__ class RD(object): @classmethod def parse(cls, buffer, length): try: data = buffer.get(length) return cls(data) except (BufferError, BimapError) as e: raise DNSError("Error unpacking RD [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, data=b"") -> None: check_bytes("data", data) self.data = bytes(data) def pack(self, buffer): buffer.append(self.data) def __repr__(self): if len(self.data) > 0: return "\\# %d %s" % ( len(self.data), binascii.hexlify(self.data).decode().upper(), ) else: return "\\# 0" attrs = ("data",) def _force_bytes(x): if isinstance(x, bytes): return x else: return x.encode() class TXT(RD): @classmethod def parse(cls, buffer, length): try: data = list() start_bo = buffer.offset now_length = 0 while buffer.offset < start_bo + length: (txtlength,) = buffer.unpack("!B") if now_length + txtlength < length: now_length += txtlength data.append(buffer.get(txtlength)) else: raise DNSError( "Invalid TXT record: len(%d) > RD len(%d)" % (txtlength, length) ) return cls(data) except (BufferError, BimapError) as e: raise DNSError("Error unpacking TXT [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, data) -> None: if type(data) in (tuple, list): self.data = [_force_bytes(x) for x in data] else: self.data = [_force_bytes(data)] if any([len(x) > 255 for x in self.data]): raise DNSError("TXT record too long: %s" % self.data) def pack(self, buffer): for ditem in self.data: if len(ditem) > 255: raise DNSError("TXT record too long: %s" % ditem) buffer.pack("!B", len(ditem)) buffer.append(ditem) def __repr__(self): return ",".join([repr(x) for x in self.data]) class A(RD): data = IP4("data") @classmethod def parse(cls, buffer, length): try: data = buffer.unpack("!BBBB") return cls(data) except (BufferError, BimapError) as e: raise DNSError("Error unpacking A [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, data) -> None: if type(data) in (tuple, list): self.data = tuple(data) else: self.data = tuple(map(int, data.rstrip(".").split("."))) def pack(self, buffer): buffer.pack("!BBBB", *self.data) def __repr__(self): return "%d.%d.%d.%d" % self.data def _parse_ipv6(a): l, _, r = a.partition("::") l_groups = list(chain(*[divmod(int(x, 16), 256) for x in l.split(":") if x])) r_groups = list(chain(*[divmod(int(x, 16), 256) for x in r.split(":") if x])) zeros = [0] * (16 - len(l_groups) - len(r_groups)) return tuple(l_groups + zeros + r_groups) def _format_ipv6(a): left = [] right = [] current = "left" for i in range(0, 16, 2): group = (a[i] << 8) + a[i + 1] if current == "left": if group == 0 and i < 14: if (a[i + 2] << 8) + a[i + 3] == 0: current = "right" else: left.append("0") else: left.append("%x" % group) else: if group == 0 and len(right) == 0: pass else: right.append("%x" % group) if len(left) < 8: return ":".join(left) + "::" + ":".join(right) else: return ":".join(left) class AAAA(RD): data = IP6("data") @classmethod def parse(cls, buffer, length): try: data = buffer.unpack("!16B") return cls(data) except (BufferError, BimapError) as e: raise DNSError("Error unpacking AAAA [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, data) -> None: if type(data) in (tuple, list): self.data = tuple(data) else: self.data = _parse_ipv6(data) def pack(self, buffer): buffer.pack("!16B", *self.data) def __repr__(self): return _format_ipv6(self.data) class CNAME(RD): @classmethod def parse(cls, buffer, length): try: label = buffer.decode_name() return cls(label) except (BufferError, BimapError) as e: raise DNSError("Error unpacking CNAME [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, label=None) -> None: self.label = label def set_label(self, label): if isinstance(label, DNSLabel): self._label = label else: self._label = DNSLabel(label) def get_label(self): return self._label label = property(get_label, set_label) def pack(self, buffer): buffer.encode_name(self.label) def __repr__(self): return "%s" % (self.label) attrs = ("label",) class PTR(CNAME): pass class SRV(RD): priority = H("priority") weight = H("weight") port = H("port") @classmethod def parse(cls, buffer, length): try: priority, weight, port = buffer.unpack("!HHH") target = buffer.decode_name() return cls(priority, weight, port, target) except (BufferError, BimapError) as e: raise DNSError("Error unpacking SRV [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, priority=0, weight=0, port=0, target=None) -> None: self.priority = priority self.weight = weight self.port = port self.target = target def set_target(self, target): if isinstance(target, DNSLabel): self._target = target else: self._target = DNSLabel(target) def get_target(self): return self._target target = property(get_target, set_target) def pack(self, buffer): buffer.pack("!HHH", self.priority, self.weight, self.port) buffer.encode_name(self.target) def __repr__(self): return "%d %d %d %s" % (self.priority, self.weight, self.port, self.target) attrs = ("priority", "weight", "port", "target") def decode_type_bitmap(type_bitmap): rrlist = [] buf = DNSBuffer(type_bitmap) while buf.remaining(): winnum, winlen = buf.unpack("BB") bitmap = bytearray(buf.get(winlen)) for (pos, value) in enumerate(bitmap): for i in range(8): if (value << i) & 0x80: bitpos = (256 * winnum) + (8 * pos) + i rrlist.append(QTYPE[bitpos]) return rrlist def encode_type_bitmap(rrlist): rrlist = sorted([getattr(QTYPE, rr) for rr in rrlist]) buf = DNSBuffer() curWindow = rrlist[0] // 256 bitmap = bytearray(32) n = len(rrlist) - 1 for i, rr in enumerate(rrlist): v = rr - curWindow * 256 bitmap[v // 8] |= 1 << (7 - v % 8) if i == n or rrlist[i + 1] >= (curWindow + 1) * 256: while bitmap[-1] == 0: bitmap = bitmap[:-1] buf.pack("BB", curWindow, len(bitmap)) buf.append(bitmap) if i != n: curWindow = rrlist[i + 1] // 256 bitmap = bytearray(32) return buf.data class NSEC(RD): @classmethod def parse(cls, buffer, length): try: end = buffer.offset + length name = buffer.decode_name() rrlist = decode_type_bitmap(buffer.get(end - buffer.offset)) return cls(name, rrlist) except (BufferError, BimapError) as e: raise DNSError("Error unpacking NSEC [offset=%d]: %s" % (buffer.offset, e)) def __init__(self, label, rrlist) -> None: self.label = label self.rrlist = rrlist def set_label(self, label): if isinstance(label, DNSLabel): self._label = label else: self._label = DNSLabel(label) def get_label(self): return self._label label = property(get_label, set_label) def pack(self, buffer): buffer.encode_name(self.label) buffer.append(encode_type_bitmap(self.rrlist)) def __repr__(self): return "%s %s" % (self.label, " ".join(self.rrlist)) attrs = ("label", "rrlist") RDMAP = {"A": A, "AAAA": AAAA, "TXT": TXT, "PTR": PTR, "SRV": SRV, "NSEC": NSEC} ================================================ FILE: copyparty/stolen/dnslib/label.py ================================================ # coding: utf-8 from __future__ import print_function import re from .bit import get_bits, set_bits from .buffer import Buffer, BufferError LDH = set(range(33, 127)) ESCAPE = re.compile(r"\\([0-9][0-9][0-9])") avahi_379 = 0 def set_avahi_379(): global avahi_379 avahi_379 = 1 def log_avahi_379(args): global avahi_379 if avahi_379 == 2: return avahi_379 = 2 t = "Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d];\n\033[35m NOTE: this is probably avahi-bug #379, packet corruption in Avahi's mDNS-reflection feature. Copyparty has a workaround and is OK, but other devices need either --zm4 or --zm6" raise BufferError(t % args) class DNSLabelError(Exception): pass class DNSLabel(object): def __init__(self, label): if type(label) == DNSLabel: self.label = label.label elif type(label) in (list, tuple): self.label = tuple(label) else: if not label or label in (b".", "."): self.label = () elif type(label) is not bytes: if type("") != type(b""): label = ESCAPE.sub(lambda m: chr(int(m[1])), label) self.label = tuple(label.encode("idna").rstrip(b".").split(b".")) else: if type("") == type(b""): label = ESCAPE.sub(lambda m: chr(int(m.groups()[0])), label) self.label = tuple(label.rstrip(b".").split(b".")) def add(self, name): new = DNSLabel(name) if self.label: new.label += self.label return new def idna(self): return ".".join([s.decode("idna") for s in self.label]) + "." def _decode(self, s): if set(s).issubset(LDH): return s.decode() else: return "".join([(chr(c) if (c in LDH) else "\\%03d" % c) for c in s]) def __str__(self): return ".".join([self._decode(bytearray(s)) for s in self.label]) + "." def __repr__(self): return "" % str(self) def __hash__(self): return hash(tuple(map(lambda x: x.lower(), self.label))) def __ne__(self, other): return not self == other def __eq__(self, other): if type(other) != DNSLabel: return self.__eq__(DNSLabel(other)) else: return [l.lower() for l in self.label] == [l.lower() for l in other.label] def __len__(self): return len(b".".join(self.label)) class DNSBuffer(Buffer): def __init__(self, data=b""): super(DNSBuffer, self).__init__(data) self.names = {} def decode_name(self, last=-1): label = [] done = False while not done: (length,) = self.unpack("!B") if get_bits(length, 6, 2) == 3: self.offset -= 1 pointer = get_bits(self.unpack("!H")[0], 0, 14) save = self.offset if last == save: raise BufferError( "Recursive pointer in DNSLabel [offset=%d,pointer=%d,length=%d]" % (self.offset, pointer, len(self.data)) ) if pointer < self.offset: self.offset = pointer elif avahi_379: log_avahi_379((self.offset, pointer, len(self.data))) label.extend(b"a") break else: raise BufferError( "Invalid pointer in DNSLabel [offset=%d,pointer=%d,length=%d]" % (self.offset, pointer, len(self.data)) ) label.extend(self.decode_name(save).label) self.offset = save done = True else: if length > 0: l = self.get(length) try: l.decode() except UnicodeDecodeError: raise BufferError("Invalid label <%s>" % l) label.append(l) else: done = True return DNSLabel(label) def encode_name(self, name): if not isinstance(name, DNSLabel): name = DNSLabel(name) if len(name) > 253: raise DNSLabelError("Domain label too long: %r" % name) name = list(name.label) while name: if tuple(name) in self.names: pointer = self.names[tuple(name)] pointer = set_bits(pointer, 3, 14, 2) self.pack("!H", pointer) return else: self.names[tuple(name)] = self.offset element = name.pop(0) if len(element) > 63: raise DNSLabelError("Label component too long: %r" % element) self.pack("!B", len(element)) self.append(element) self.append(b"\x00") def encode_name_nocompress(self, name): if not isinstance(name, DNSLabel): name = DNSLabel(name) if len(name) > 253: raise DNSLabelError("Domain label too long: %r" % name) name = list(name.label) while name: element = name.pop(0) if len(element) > 63: raise DNSLabelError("Label component too long: %r" % element) self.pack("!B", len(element)) self.append(element) self.append(b"\x00") ================================================ FILE: copyparty/stolen/dnslib/lex.py ================================================ # coding: utf-8 from __future__ import print_function import collections try: from StringIO import StringIO except ImportError: from io import StringIO class Lexer(object): escape_chars = "\\" escape = {"n": "\n", "t": "\t", "r": "\r"} def __init__(self, f, debug=False): if hasattr(f, "read"): self.f = f elif type(f) == str: self.f = StringIO(f) elif type(f) == bytes: self.f = StringIO(f.decode()) else: raise ValueError("Invalid input") self.debug = debug self.q = collections.deque() self.state = self.lexStart self.escaped = False self.eof = False def __iter__(self): return self.parse() def next_token(self): if self.debug: print("STATE", self.state) (tok, self.state) = self.state() return tok def parse(self): while self.state is not None and not self.eof: tok = self.next_token() if tok: yield tok def read(self, n=1): s = "" while self.q and n > 0: s += self.q.popleft() n -= 1 s += self.f.read(n) if s == "": self.eof = True if self.debug: print("Read: >%s<" % repr(s)) return s def peek(self, n=1): s = "" i = 0 while len(self.q) > i and n > 0: s += self.q[i] i += 1 n -= 1 r = self.f.read(n) if n > 0 and r == "": self.eof = True self.q.extend(r) if self.debug: print("Peek : >%s<" % repr(s + r)) return s + r def pushback(self, s): p = collections.deque(s) p.extend(self.q) self.q = p def readescaped(self): c = self.read(1) if c in self.escape_chars: self.escaped = True n = self.peek(3) if n.isdigit(): n = self.read(3) if self.debug: print("Escape: >%s<" % n) return chr(int(n, 8)) elif n[0] in "x": x = self.read(3) if self.debug: print("Escape: >%s<" % x) return chr(int(x[1:], 16)) else: c = self.read(1) if self.debug: print("Escape: >%s<" % c) return self.escape.get(c, c) else: self.escaped = False return c def lexStart(self): return (None, None) ================================================ FILE: copyparty/stolen/dnslib/ranges.py ================================================ # coding: utf-8 import sys if sys.version_info < (3,): int_types = ( int, long, ) byte_types = (str, bytearray) else: int_types = (int,) byte_types = (bytes, bytearray) def check_instance(name, val, types): if not isinstance(val, types): raise ValueError( "Attribute '%s' must be instance of %s [%s]" % (name, types, type(val)) ) def check_bytes(name, val): return check_instance(name, val, byte_types) def range_property(attr, min, max): def getter(obj): return getattr(obj, "_%s" % attr) def setter(obj, val): if isinstance(val, int_types) and min <= val <= max: setattr(obj, "_%s" % attr, val) else: raise ValueError( "Attribute '%s' must be between %d-%d [%s]" % (attr, min, max, val) ) return property(getter, setter) def B(attr): return range_property(attr, 0, 255) def H(attr): return range_property(attr, 0, 65535) def I(attr): return range_property(attr, 0, 4294967295) def ntuple_range(attr, n, min, max): f = lambda x: isinstance(x, int_types) and min <= x <= max def getter(obj): return getattr(obj, "_%s" % attr) def setter(obj, val): if len(val) != n: raise ValueError( "Attribute '%s' must be tuple with %d elements [%s]" % (attr, n, val) ) if all(map(f, val)): setattr(obj, "_%s" % attr, val) else: raise ValueError( "Attribute '%s' elements must be between %d-%d [%s]" % (attr, min, max, val) ) return property(getter, setter) def IP4(attr): return ntuple_range(attr, 4, 0, 255) def IP6(attr): return ntuple_range(attr, 16, 0, 255) ================================================ FILE: copyparty/stolen/ifaddr/README.md ================================================ `ifaddr` with py2.7 support enabled by make-sfx.sh which strips py3 hints using strip_hints and removes the `^if True:` blocks L: BSD-2-Clause Copyright (c) 2014 Stefan C. Mueller https://github.com/pydron/ifaddr/ ================================================ FILE: copyparty/stolen/ifaddr/__init__.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals """ L: BSD-2-Clause Copyright (c) 2014 Stefan C. Mueller https://github.com/pydron/ifaddr/tree/0.2.0 """ import os import platform from ._shared import IP, Adapter def nope(include_unconfigured=False): return [] host_os = platform.system() machine = platform.machine() py_impl = platform.python_implementation() if os.environ.get("PRTY_NO_IFADDR"): get_adapters = nope elif machine in ("s390x",) or host_os in ("IRIX32",): # s390x deadlocks at libc.getifaddrs # irix libc does not have getifaddrs at all print("ifaddr unavailable; can't determine LAN IP: unsupported OS") get_adapters = nope elif py_impl in ("GraalVM",): print("ifaddr unavailable; can't determine LAN IP: unsupported interpreter") get_adapters = nope elif os.name == "nt": from ._win32 import get_adapters elif os.name == "posix": from ._posix import get_adapters else: print("ifaddr unavailable; can't determine LAN IP: unsupported OS") get_adapters = nope __all__ = ["Adapter", "IP", "get_adapters"] ================================================ FILE: copyparty/stolen/ifaddr/_posix.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import collections import ctypes.util import os import socket import ipaddress if True: # pylint: disable=using-constant-test from typing import Iterable, Optional from . import _shared as shared from ._shared import U class ifaddrs(ctypes.Structure): pass ifaddrs._fields_ = [ ("ifa_next", ctypes.POINTER(ifaddrs)), ("ifa_name", ctypes.c_char_p), ("ifa_flags", ctypes.c_uint), ("ifa_addr", ctypes.POINTER(shared.sockaddr)), ("ifa_netmask", ctypes.POINTER(shared.sockaddr)), ] libc = ctypes.CDLL(ctypes.util.find_library("socket" if os.uname()[0] == "SunOS" else "c"), use_errno=True) # type: ignore def get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]: addr0 = addr = ctypes.POINTER(ifaddrs)() retval = libc.getifaddrs(ctypes.byref(addr)) if retval != 0: eno = ctypes.get_errno() raise OSError(eno, os.strerror(eno)) ips = collections.OrderedDict() def add_ip(adapter_name: str, ip: Optional[shared.IP]) -> None: if adapter_name not in ips: index = None # type: Optional[int] try: # Mypy errors on this when the Windows CI runs: # error: Module has no attribute "if_nametoindex" index = socket.if_nametoindex(adapter_name) # type: ignore except (OSError, AttributeError): pass ips[adapter_name] = shared.Adapter( adapter_name, adapter_name, [], index=index ) if ip is not None: ips[adapter_name].ips.append(ip) while addr: name = addr[0].ifa_name.decode(encoding="UTF-8") ip_addr = shared.sockaddr_to_ip(addr[0].ifa_addr) if ip_addr: if addr[0].ifa_netmask and not addr[0].ifa_netmask[0].sa_familiy: addr[0].ifa_netmask[0].sa_familiy = addr[0].ifa_addr[0].sa_familiy netmask = shared.sockaddr_to_ip(addr[0].ifa_netmask) if isinstance(netmask, tuple): netmaskStr = U(netmask[0]) prefixlen = shared.ipv6_prefixlength(ipaddress.IPv6Address(netmaskStr)) else: if netmask is None: t = "sockaddr_to_ip({}) returned None" raise Exception(t.format(addr[0].ifa_netmask)) netmaskStr = U("0.0.0.0/" + netmask) prefixlen = ipaddress.IPv4Network(netmaskStr).prefixlen ip = shared.IP(ip_addr, prefixlen, name) add_ip(name, ip) else: if include_unconfigured: add_ip(name, None) addr = addr[0].ifa_next libc.freeifaddrs(addr0) return ips.values() ================================================ FILE: copyparty/stolen/ifaddr/_shared.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import ctypes import platform import socket import sys import ipaddress if True: # pylint: disable=using-constant-test from typing import Callable, List, Optional, Union PY2 = sys.version_info < (3,) if not PY2: U: Callable[[str], str] = str else: U = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable range = xrange # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable class Adapter(object): """ Represents a network interface device controller (NIC), such as a network card. An adapter can have multiple IPs. On Linux aliasing (multiple IPs per physical NIC) is implemented by creating 'virtual' adapters, each represented by an instance of this class. Each of those 'virtual' adapters can have both a IPv4 and an IPv6 IP address. """ def __init__( self, name: str, nice_name: str, ips: List["IP"], index: Optional[int] = None ) -> None: #: Unique name that identifies the adapter in the system. #: On Linux this is of the form of `eth0` or `eth0:1`, on #: Windows it is a UUID in string representation, such as #: `{846EE342-7039-11DE-9D20-806E6F6E6963}`. self.name = name #: Human readable name of the adpater. On Linux this #: is currently the same as :attr:`name`. On Windows #: this is the name of the device. self.nice_name = nice_name #: List of :class:`ifaddr.IP` instances in the order they were #: reported by the system. self.ips = ips #: Adapter index as used by some API (e.g. IPv6 multicast group join). self.index = index def __repr__(self) -> str: return "Adapter(name={name}, nice_name={nice_name}, ips={ips}, index={index})".format( name=repr(self.name), nice_name=repr(self.nice_name), ips=repr(self.ips), index=repr(self.index), ) if True: # pylint: disable=using-constant-test # Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format) _IPv4Address = str # Type of an IPv6 address (a three-tuple `(ip, flowinfo, scope_id)`) _IPv6Address = tuple[str, int, int] class IP(object): """ Represents an IP address of an adapter. """ def __init__( self, ip: Union[_IPv4Address, _IPv6Address], network_prefix: int, nice_name: str ) -> None: #: IP address. For IPv4 addresses this is a string in #: "xxx.xxx.xxx.xxx" format. For IPv6 addresses this #: is a three-tuple `(ip, flowinfo, scope_id)`, where #: `ip` is a string in the usual collon separated #: hex format. self.ip = ip #: Number of bits of the IP that represent the #: network. For a `255.255.255.0` netmask, this #: number would be `24`. self.network_prefix = network_prefix #: Human readable name for this IP. #: On Linux is this currently the same as the adapter name. #: On Windows this is the name of the network connection #: as configured in the system control panel. self.nice_name = nice_name @property def is_IPv4(self) -> bool: """ Returns `True` if this IP is an IPv4 address and `False` if it is an IPv6 address. """ return not isinstance(self.ip, tuple) @property def is_IPv6(self) -> bool: """ Returns `True` if this IP is an IPv6 address and `False` if it is an IPv4 address. """ return isinstance(self.ip, tuple) def __repr__(self) -> str: return "IP(ip={ip}, network_prefix={network_prefix}, nice_name={nice_name})".format( ip=repr(self.ip), network_prefix=repr(self.network_prefix), nice_name=repr(self.nice_name), ) if platform.system() == "Darwin" or "BSD" in platform.system(): # BSD derived systems use marginally different structures # than either Linux or Windows. # I still keep it in `shared` since we can use # both structures equally. class sockaddr(ctypes.Structure): _fields_ = [ ("sa_len", ctypes.c_uint8), ("sa_familiy", ctypes.c_uint8), ("sa_data", ctypes.c_uint8 * 14), ] class sockaddr_in(ctypes.Structure): _fields_ = [ ("sa_len", ctypes.c_uint8), ("sa_familiy", ctypes.c_uint8), ("sin_port", ctypes.c_uint16), ("sin_addr", ctypes.c_uint8 * 4), ("sin_zero", ctypes.c_uint8 * 8), ] class sockaddr_in6(ctypes.Structure): _fields_ = [ ("sa_len", ctypes.c_uint8), ("sa_familiy", ctypes.c_uint8), ("sin6_port", ctypes.c_uint16), ("sin6_flowinfo", ctypes.c_uint32), ("sin6_addr", ctypes.c_uint8 * 16), ("sin6_scope_id", ctypes.c_uint32), ] else: class sockaddr(ctypes.Structure): # type: ignore _fields_ = [("sa_familiy", ctypes.c_uint16), ("sa_data", ctypes.c_uint8 * 14)] class sockaddr_in(ctypes.Structure): # type: ignore _fields_ = [ ("sin_familiy", ctypes.c_uint16), ("sin_port", ctypes.c_uint16), ("sin_addr", ctypes.c_uint8 * 4), ("sin_zero", ctypes.c_uint8 * 8), ] class sockaddr_in6(ctypes.Structure): # type: ignore _fields_ = [ ("sin6_familiy", ctypes.c_uint16), ("sin6_port", ctypes.c_uint16), ("sin6_flowinfo", ctypes.c_uint32), ("sin6_addr", ctypes.c_uint8 * 16), ("sin6_scope_id", ctypes.c_uint32), ] def sockaddr_to_ip( sockaddr_ptr: "ctypes.pointer[sockaddr]", ) -> Optional[Union[_IPv4Address, _IPv6Address]]: if sockaddr_ptr: if sockaddr_ptr[0].sa_familiy == socket.AF_INET: ipv4 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in)) ippacked = bytes(bytearray(ipv4[0].sin_addr)) ip = U(ipaddress.ip_address(ippacked)) return ip elif sockaddr_ptr[0].sa_familiy == socket.AF_INET6: ipv6 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in6)) flowinfo = ipv6[0].sin6_flowinfo ippacked = bytes(bytearray(ipv6[0].sin6_addr)) ip = U(ipaddress.ip_address(ippacked)) scope_id = ipv6[0].sin6_scope_id return (ip, flowinfo, scope_id) return None def ipv6_prefixlength(address: ipaddress.IPv6Address) -> int: prefix_length = 0 for i in range(address.max_prefixlen): if int(address) >> i & 1: prefix_length = prefix_length + 1 return prefix_length ================================================ FILE: copyparty/stolen/ifaddr/_win32.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import ctypes from ctypes import wintypes if True: # pylint: disable=using-constant-test from typing import Iterable, List from . import _shared as shared NO_ERROR = 0 ERROR_BUFFER_OVERFLOW = 111 MAX_ADAPTER_NAME_LENGTH = 256 MAX_ADAPTER_DESCRIPTION_LENGTH = 128 MAX_ADAPTER_ADDRESS_LENGTH = 8 AF_UNSPEC = 0 class SOCKET_ADDRESS(ctypes.Structure): _fields_ = [ ("lpSockaddr", ctypes.POINTER(shared.sockaddr)), ("iSockaddrLength", wintypes.INT), ] class IP_ADAPTER_UNICAST_ADDRESS(ctypes.Structure): pass IP_ADAPTER_UNICAST_ADDRESS._fields_ = [ ("Length", wintypes.ULONG), ("Flags", wintypes.DWORD), ("Next", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)), ("Address", SOCKET_ADDRESS), ("PrefixOrigin", ctypes.c_uint), ("SuffixOrigin", ctypes.c_uint), ("DadState", ctypes.c_uint), ("ValidLifetime", wintypes.ULONG), ("PreferredLifetime", wintypes.ULONG), ("LeaseLifetime", wintypes.ULONG), ("OnLinkPrefixLength", ctypes.c_uint8), ] class IP_ADAPTER_ADDRESSES(ctypes.Structure): pass IP_ADAPTER_ADDRESSES._fields_ = [ ("Length", wintypes.ULONG), ("IfIndex", wintypes.DWORD), ("Next", ctypes.POINTER(IP_ADAPTER_ADDRESSES)), ("AdapterName", ctypes.c_char_p), ("FirstUnicastAddress", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)), ("FirstAnycastAddress", ctypes.c_void_p), ("FirstMulticastAddress", ctypes.c_void_p), ("FirstDnsServerAddress", ctypes.c_void_p), ("DnsSuffix", ctypes.c_wchar_p), ("Description", ctypes.c_wchar_p), ("FriendlyName", ctypes.c_wchar_p), ] iphlpapi = ctypes.windll.LoadLibrary("Iphlpapi") # type: ignore def enumerate_interfaces_of_adapter( nice_name: str, address: IP_ADAPTER_UNICAST_ADDRESS ) -> Iterable[shared.IP]: # Iterate through linked list and fill list addresses = [] # type: List[IP_ADAPTER_UNICAST_ADDRESS] while True: addresses.append(address) if not address.Next: break address = address.Next[0] for address in addresses: ip = shared.sockaddr_to_ip(address.Address.lpSockaddr) if ip is None: t = "sockaddr_to_ip({}) returned None" raise Exception(t.format(address.Address.lpSockaddr)) network_prefix = address.OnLinkPrefixLength yield shared.IP(ip, network_prefix, nice_name) def get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]: # Call GetAdaptersAddresses() with error and buffer size handling addressbuffersize = wintypes.ULONG(15 * 1024) retval = ERROR_BUFFER_OVERFLOW while retval == ERROR_BUFFER_OVERFLOW: addressbuffer = ctypes.create_string_buffer(addressbuffersize.value) retval = iphlpapi.GetAdaptersAddresses( wintypes.ULONG(AF_UNSPEC), wintypes.ULONG(0), None, ctypes.byref(addressbuffer), ctypes.byref(addressbuffersize), ) if retval != NO_ERROR: raise ctypes.WinError() # type: ignore # Iterate through adapters fill array address_infos = [] # type: List[IP_ADAPTER_ADDRESSES] address_info = IP_ADAPTER_ADDRESSES.from_buffer(addressbuffer) while True: address_infos.append(address_info) if not address_info.Next: break address_info = address_info.Next[0] # Iterate through unicast addresses result = [] # type: List[shared.Adapter] for adapter_info in address_infos: # We don't expect non-ascii characters here, so encoding shouldn't matter name = adapter_info.AdapterName.decode() nice_name = adapter_info.Description index = adapter_info.IfIndex if adapter_info.FirstUnicastAddress: ips = enumerate_interfaces_of_adapter( adapter_info.FriendlyName, adapter_info.FirstUnicastAddress[0] ) ips = list(ips) result.append(shared.Adapter(name, nice_name, ips, index=index)) elif include_unconfigured: result.append(shared.Adapter(name, nice_name, [], index=index)) return result ================================================ FILE: copyparty/stolen/qrcodegen.py ================================================ # coding: utf-8 # modified copy of Project Nayuki's qrcodegen (MIT-licensed); # https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py # the original ^ is extremely well commented so refer to that for explanations # hacks: binary-only, auto-ecc, py2-compat from __future__ import print_function, unicode_literals import collections import itertools if True: # pylint: disable=using-constant-test from collections.abc import Sequence from typing import Callable, List, Optional, Tuple, Union try: range = xrange except: pass def num_char_count_bits(ver: int) -> int: return 16 if (ver + 7) // 17 else 8 class Ecc(object): ordinal: int formatbits: int def __init__(self, i: int, fb: int) -> None: self.ordinal = i self.formatbits = fb LOW: "Ecc" MEDIUM: "Ecc" QUARTILE: "Ecc" HIGH: "Ecc" Ecc.LOW = Ecc(0, 1) Ecc.MEDIUM = Ecc(1, 0) Ecc.QUARTILE = Ecc(2, 3) Ecc.HIGH = Ecc(3, 2) class QrSegment(object): @staticmethod def make_seg(data: Union[bytes, Sequence[int]]) -> "QrSegment": bb = _BitBuffer() for b in data: bb.append_bits(b, 8) return QrSegment(len(data), bb) numchars: int # num bytes, not the same as the data's bit length bitdata: List[int] # The data bits of this segment def __init__(self, numch: int, bitdata: Sequence[int]) -> None: if numch < 0: raise ValueError() self.numchars = numch self.bitdata = list(bitdata) @staticmethod def get_total_bits(segs: Sequence["QrSegment"], ver: int) -> Optional[int]: result = 0 for seg in segs: ccbits: int = num_char_count_bits(ver) if seg.numchars >= (1 << ccbits): return None # segment length doesn't fit the field's bit width result += 4 + ccbits + len(seg.bitdata) return result class QrCode(object): @staticmethod def encode_binary(data: Union[bytes, Sequence[int]]) -> "QrCode": return QrCode.encode_segments([QrSegment.make_seg(data)]) @staticmethod def encode_segments( segs: Sequence[QrSegment], ecl: Ecc = Ecc.LOW, minver: int = 2, maxver: int = 40, mask: int = -1, ) -> "QrCode": for ver in range(minver, maxver + 1): datacapacitybits: int = QrCode._get_num_data_codewords(ver, ecl) * 8 datausedbits: Optional[int] = QrSegment.get_total_bits(segs, ver) if (datausedbits is not None) and (datausedbits <= datacapacitybits): break assert datausedbits for newecl in ( Ecc.MEDIUM, Ecc.QUARTILE, Ecc.HIGH, ): if datausedbits <= QrCode._get_num_data_codewords(ver, newecl) * 8: ecl = newecl # Concatenate all segments to create the data bit string bb = _BitBuffer() for seg in segs: bb.append_bits(4, 4) bb.append_bits(seg.numchars, num_char_count_bits(ver)) bb.extend(seg.bitdata) assert len(bb) == datausedbits # Add terminator and pad up to a byte if applicable datacapacitybits = QrCode._get_num_data_codewords(ver, ecl) * 8 assert len(bb) <= datacapacitybits bb.append_bits(0, min(4, datacapacitybits - len(bb))) bb.append_bits(0, -len(bb) % 8) assert len(bb) % 8 == 0 # Pad with alternating bytes until data capacity is reached for padbyte in itertools.cycle((0xEC, 0x11)): if len(bb) >= datacapacitybits: break bb.append_bits(padbyte, 8) # Pack bits into bytes in big endian datacodewords = bytearray([0] * (len(bb) // 8)) for (i, bit) in enumerate(bb): datacodewords[i >> 3] |= bit << (7 - (i & 7)) return QrCode(ver, ecl, datacodewords, mask) ver: int size: int # w/h; 21..177 (ver * 4 + 17) ecclvl: Ecc mask: int # 0..7 modules: List[List[bool]] unmaskable: List[List[bool]] def __init__( self, ver: int, ecclvl: Ecc, datacodewords: Union[bytes, Sequence[int]], msk: int, ) -> None: self.ver = ver self.size = ver * 4 + 17 self.ecclvl = ecclvl self.modules = [[False] * self.size for _ in range(self.size)] self.unmaskable = [[False] * self.size for _ in range(self.size)] # Compute ECC, draw modules self._draw_function_patterns() allcodewords: bytes = self._add_ecc_and_interleave(bytearray(datacodewords)) self._draw_codewords(allcodewords) if msk == -1: # automask minpenalty: int = 1 << 32 for i in range(8): self._apply_mask(i) self._draw_format_bits(i) penalty = self._get_penalty_score() if penalty < minpenalty: msk = i minpenalty = penalty self._apply_mask(i) # xor/undo assert 0 <= msk <= 7 self.mask = msk self._apply_mask(msk) # Apply the final choice of mask self._draw_format_bits(msk) # Overwrite old format bits def _draw_function_patterns(self) -> None: # Draw horizontal and vertical timing patterns for i in range(self.size): self._set_function_module(6, i, i % 2 == 0) self._set_function_module(i, 6, i % 2 == 0) # Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) self._draw_finder_pattern(3, 3) self._draw_finder_pattern(self.size - 4, 3) self._draw_finder_pattern(3, self.size - 4) # Draw numerous alignment patterns alignpatpos: List[int] = self._get_alignment_pattern_positions() numalign: int = len(alignpatpos) skips: Sequence[Tuple[int, int]] = ( (0, 0), (0, numalign - 1), (numalign - 1, 0), ) for i in range(numalign): for j in range(numalign): if (i, j) not in skips: # avoid finder corners self._draw_alignment_pattern(alignpatpos[i], alignpatpos[j]) # draw config data with dummy mask value; ctor overwrites it self._draw_format_bits(0) self._draw_ver() def _draw_format_bits(self, mask: int) -> None: # Calculate error correction code and pack bits; ecclvl is uint2, mask is uint3 data: int = self.ecclvl.formatbits << 3 | mask rem: int = data for _ in range(10): rem = (rem << 1) ^ ((rem >> 9) * 0x537) bits: int = (data << 10 | rem) ^ 0x5412 # uint15 assert bits >> 15 == 0 # first copy for i in range(0, 6): self._set_function_module(8, i, _get_bit(bits, i)) self._set_function_module(8, 7, _get_bit(bits, 6)) self._set_function_module(8, 8, _get_bit(bits, 7)) self._set_function_module(7, 8, _get_bit(bits, 8)) for i in range(9, 15): self._set_function_module(14 - i, 8, _get_bit(bits, i)) # second copy for i in range(0, 8): self._set_function_module(self.size - 1 - i, 8, _get_bit(bits, i)) for i in range(8, 15): self._set_function_module(8, self.size - 15 + i, _get_bit(bits, i)) self._set_function_module(8, self.size - 8, True) # Always dark def _draw_ver(self) -> None: if self.ver < 7: return # Calculate error correction code and pack bits rem: int = self.ver # ver is uint6, 7..40 for _ in range(12): rem = (rem << 1) ^ ((rem >> 11) * 0x1F25) bits: int = self.ver << 12 | rem # uint18 assert bits >> 18 == 0 # Draw two copies for i in range(18): bit: bool = _get_bit(bits, i) a: int = self.size - 11 + i % 3 b: int = i // 3 self._set_function_module(a, b, bit) self._set_function_module(b, a, bit) def _draw_finder_pattern(self, x: int, y: int) -> None: for dy in range(-4, 5): for dx in range(-4, 5): xx, yy = x + dx, y + dy if (0 <= xx < self.size) and (0 <= yy < self.size): # Chebyshev/infinity norm self._set_function_module( xx, yy, max(abs(dx), abs(dy)) not in (2, 4) ) def _draw_alignment_pattern(self, x: int, y: int) -> None: for dy in range(-2, 3): for dx in range(-2, 3): self._set_function_module(x + dx, y + dy, max(abs(dx), abs(dy)) != 1) def _set_function_module(self, x: int, y: int, isdark: bool) -> None: self.modules[y][x] = isdark self.unmaskable[y][x] = True def _add_ecc_and_interleave(self, data: bytearray) -> bytes: ver: int = self.ver assert len(data) == QrCode._get_num_data_codewords(ver, self.ecclvl) # Calculate parameter numbers numblocks: int = QrCode._NUM_ERROR_CORRECTION_BLOCKS[self.ecclvl.ordinal][ver] blockecclen: int = QrCode._ECC_CODEWORDS_PER_BLOCK[self.ecclvl.ordinal][ver] rawcodewords: int = QrCode._get_num_raw_data_modules(ver) // 8 numshortblocks: int = numblocks - rawcodewords % numblocks shortblocklen: int = rawcodewords // numblocks # Split data into blocks and append ECC to each block blocks: List[bytes] = [] rsdiv: bytes = QrCode._reed_solomon_compute_divisor(blockecclen) k: int = 0 for i in range(numblocks): dat: bytearray = data[ k : k + shortblocklen - blockecclen + (0 if i < numshortblocks else 1) ] k += len(dat) ecc: bytes = QrCode._reed_solomon_compute_remainder(dat, rsdiv) if i < numshortblocks: dat.append(0) blocks.append(dat + ecc) assert k == len(data) # Interleave (not concatenate) the bytes from every block into a single sequence result = bytearray() for i in range(len(blocks[0])): for (j, blk) in enumerate(blocks): # Skip the padding byte in short blocks if (i != shortblocklen - blockecclen) or (j >= numshortblocks): result.append(blk[i]) assert len(result) == rawcodewords return result def _draw_codewords(self, data: bytes) -> None: assert len(data) == QrCode._get_num_raw_data_modules(self.ver) // 8 i: int = 0 # Bit index into the data for right in range(self.size - 1, 0, -2): # idx of right column in each column pair if right <= 6: right -= 1 for vert in range(self.size): # Vertical counter for j in range(2): x: int = right - j upward: bool = (right + 1) & 2 == 0 y: int = (self.size - 1 - vert) if upward else vert if (not self.unmaskable[y][x]) and (i < len(data) * 8): self.modules[y][x] = _get_bit(data[i >> 3], 7 - (i & 7)) i += 1 # any remainder bits (0..7) were set 0/false/light by ctor assert i == len(data) * 8 def _apply_mask(self, mask: int) -> None: masker: Callable[[int, int], int] = QrCode._MASK_PATTERNS[mask] for y in range(self.size): for x in range(self.size): self.modules[y][x] ^= (masker(x, y) == 0) and ( not self.unmaskable[y][x] ) def _get_penalty_score(self) -> int: result: int = 0 size: int = self.size modules: List[List[bool]] = self.modules # Adjacent modules in row having same color, and finder-like patterns for y in range(size): runcolor: bool = False runx: int = 0 runhistory = collections.deque([0] * 7, 7) for x in range(size): if modules[y][x] == runcolor: runx += 1 if runx == 5: result += QrCode._PENALTY_N1 elif runx > 5: result += 1 else: self._finder_penalty_add_history(runx, runhistory) if not runcolor: result += ( self._finder_penalty_count_patterns(runhistory) * QrCode._PENALTY_N3 ) runcolor = modules[y][x] runx = 1 result += ( self._finder_penalty_terminate_and_count(runcolor, runx, runhistory) * QrCode._PENALTY_N3 ) # Adjacent modules in column having same color, and finder-like patterns for x in range(size): runcolor = False runy = 0 runhistory = collections.deque([0] * 7, 7) for y in range(size): if modules[y][x] == runcolor: runy += 1 if runy == 5: result += QrCode._PENALTY_N1 elif runy > 5: result += 1 else: self._finder_penalty_add_history(runy, runhistory) if not runcolor: result += ( self._finder_penalty_count_patterns(runhistory) * QrCode._PENALTY_N3 ) runcolor = modules[y][x] runy = 1 result += ( self._finder_penalty_terminate_and_count(runcolor, runy, runhistory) * QrCode._PENALTY_N3 ) # 2*2 blocks of modules having same color for y in range(size - 1): for x in range(size - 1): if ( modules[y][x] == modules[y][x + 1] == modules[y + 1][x] == modules[y + 1][x + 1] ): result += QrCode._PENALTY_N2 # Balance of dark and light modules dark: int = sum((1 if cell else 0) for row in modules for cell in row) total: int = size ** 2 # Note that size is odd, so dark/total != 1/2 # Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% k: int = (abs(dark * 20 - total * 10) + total - 1) // total - 1 assert 0 <= k <= 9 result += k * QrCode._PENALTY_N4 assert 0 <= result <= 2568888 # ^ Non-tight upper bound based on default values of PENALTY_N1, ..., N4 return result def _get_alignment_pattern_positions(self) -> List[int]: ver: int = self.ver if ver == 1: return [] numalign: int = ver // 7 + 2 step: int = ( 26 if (ver == 32) else (ver * 4 + numalign * 2 + 1) // (numalign * 2 - 2) * 2 ) result: List[int] = [ (self.size - 7 - i * step) for i in range(numalign - 1) ] + [6] return list(reversed(result)) @staticmethod def _get_num_raw_data_modules(ver: int) -> int: result: int = (16 * ver + 128) * ver + 64 if ver >= 2: numalign: int = ver // 7 + 2 result -= (25 * numalign - 10) * numalign - 55 if ver >= 7: result -= 36 assert 208 <= result <= 29648 return result @staticmethod def _get_num_data_codewords(ver: int, ecl: Ecc) -> int: return ( QrCode._get_num_raw_data_modules(ver) // 8 - QrCode._ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] * QrCode._NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver] ) @staticmethod def _reed_solomon_compute_divisor(degree: int) -> bytes: if not (1 <= degree <= 255): raise ValueError("Degree out of range") # Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. # For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93]. result = bytearray([0] * (degree - 1) + [1]) # start with monomial x^0 # Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), # and drop the highest monomial term which is always 1x^degree. # Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). root: int = 1 for _ in range(degree): # Multiply the current product by (x - r^i) for j in range(degree): result[j] = QrCode._reed_solomon_multiply(result[j], root) if j + 1 < degree: result[j] ^= result[j + 1] root = QrCode._reed_solomon_multiply(root, 0x02) return result @staticmethod def _reed_solomon_compute_remainder(data: bytes, divisor: bytes) -> bytes: result = bytearray([0] * len(divisor)) for b in data: # Polynomial division factor: int = b ^ result.pop(0) result.append(0) for (i, coef) in enumerate(divisor): result[i] ^= QrCode._reed_solomon_multiply(coef, factor) return result @staticmethod def _reed_solomon_multiply(x: int, y: int) -> int: if (x >> 8 != 0) or (y >> 8 != 0): raise ValueError("Byte out of range") z: int = 0 # Russian peasant multiplication for i in reversed(range(8)): z = (z << 1) ^ ((z >> 7) * 0x11D) z ^= ((y >> i) & 1) * x assert z >> 8 == 0 return z def _finder_penalty_count_patterns(self, runhistory: collections.deque[int]) -> int: n: int = runhistory[1] assert n <= self.size * 3 core: bool = ( n > 0 and (runhistory[2] == runhistory[4] == runhistory[5] == n) and runhistory[3] == n * 3 ) return ( 1 if (core and runhistory[0] >= n * 4 and runhistory[6] >= n) else 0 ) + (1 if (core and runhistory[6] >= n * 4 and runhistory[0] >= n) else 0) def _finder_penalty_terminate_and_count( self, currentruncolor: bool, currentrunlength: int, runhistory: collections.deque[int], ) -> int: if currentruncolor: # Terminate dark run self._finder_penalty_add_history(currentrunlength, runhistory) currentrunlength = 0 currentrunlength += self.size # Add light border to final run self._finder_penalty_add_history(currentrunlength, runhistory) return self._finder_penalty_count_patterns(runhistory) def _finder_penalty_add_history( self, currentrunlength: int, runhistory: collections.deque[int] ) -> None: if runhistory[0] == 0: currentrunlength += self.size # Add light border to initial run runhistory.appendleft(currentrunlength) _PENALTY_N1: int = 3 _PENALTY_N2: int = 3 _PENALTY_N3: int = 40 _PENALTY_N4: int = 10 # fmt: off _ECC_CODEWORDS_PER_BLOCK: Sequence[Sequence[int]] = ( (-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30), # noqa: E241 # L (-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28), # noqa: E241 # M (-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30), # noqa: E241 # Q (-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30)) # noqa: E241 # H _NUM_ERROR_CORRECTION_BLOCKS: Sequence[Sequence[int]] = ( (-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25), # noqa: E241 # L (-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49), # noqa: E241 # M (-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68), # noqa: E241 # Q (-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81)) # noqa: E241 # H # fmt: on _MASK_PATTERNS: Sequence[Callable[[int, int], int]] = ( (lambda x, y: (x + y) % 2), (lambda x, y: y % 2), (lambda x, y: x % 3), (lambda x, y: (x + y) % 3), (lambda x, y: (x // 3 + y // 2) % 2), (lambda x, y: x * y % 2 + x * y % 3), (lambda x, y: (x * y % 2 + x * y % 3) % 2), (lambda x, y: ((x + y) % 2 + x * y % 3) % 2), ) class _BitBuffer(list): # type: ignore def append_bits(self, val: int, n: int) -> None: if (n < 0) or (val >> n != 0): raise ValueError("Value out of range") self.extend(((val >> i) & 1) for i in reversed(range(n))) def _get_bit(x: int, i: int) -> bool: return (x >> i) & 1 != 0 class DataTooLongError(ValueError): pass ================================================ FILE: copyparty/stolen/surrogateescape.py ================================================ # coding: utf-8 """ This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error handler of Python 3. Scissored from the python-future module to avoid 4.4MB of additional dependencies: https://github.com/PythonCharmers/python-future/blob/e12549c42ed3a38ece45b9d88c75f5f3ee4d658d/src/future/utils/surrogateescape.py Original source: misc/python/surrogateescape.py in https://bitbucket.org/haypo/misc """ # This code is released under the Python license and the BSD 2-clause license import codecs import platform import sys PY3 = sys.version_info > (3,) WINDOWS = platform.system() == "Windows" FS_ERRORS = "surrogateescape" if True: # pylint: disable=using-constant-test from typing import Any if PY3: _unichr = chr bytes_chr = lambda code: bytes((code,)) else: _unichr = unichr bytes_chr = chr def surrogateescape_handler(exc: Any) -> tuple[str, int]: """ Pure Python implementation of the PEP 383: the "surrogateescape" error handler of Python 3. Undecodable bytes will be replaced by a Unicode character U+DCxx on decoding, and these are translated into the original bytes on encoding. """ mystring = exc.object[exc.start : exc.end] try: if isinstance(exc, UnicodeDecodeError): # mystring is a byte-string in this case decoded = replace_surrogate_decode(mystring) elif isinstance(exc, UnicodeEncodeError): # In the case of u'\udcc3'.encode('ascii', # 'this_surrogateescape_handler'), both Python 2.x and 3.x raise an # exception anyway after this function is called, even though I think # it's doing what it should. It seems that the strict encoder is called # to encode the unicode string that this function returns ... decoded = replace_surrogate_encode(mystring) else: raise exc except NotASurrogateError: raise exc return (decoded, exc.end) class NotASurrogateError(Exception): pass def replace_surrogate_encode(mystring: str) -> str: """ Returns a (unicode) string, not the more logical bytes, because the codecs register_error functionality expects this. """ decoded = [] for ch in mystring: code = ord(ch) # The following magic comes from Py3.3's Python/codecs.c file: if not 0xD800 <= code <= 0xDCFF: # Not a surrogate. Fail with the original exception. raise NotASurrogateError # mybytes = [0xe0 | (code >> 12), # 0x80 | ((code >> 6) & 0x3f), # 0x80 | (code & 0x3f)] # Is this a good idea? if 0xDC00 <= code <= 0xDC7F: decoded.append(_unichr(code - 0xDC00)) elif code <= 0xDCFF: decoded.append(_unichr(code - 0xDC00)) else: raise NotASurrogateError return str().join(decoded) def replace_surrogate_decode(mybytes: bytes) -> str: """ Returns a (unicode) string """ decoded = [] for ch in mybytes: # We may be parsing newbytes (in which case ch is an int) or a native # str on Py2 if isinstance(ch, int): code = ch else: code = ord(ch) if 0x80 <= code <= 0xFF: decoded.append(_unichr(0xDC00 + code)) elif code <= 0x7F: decoded.append(_unichr(code)) else: raise NotASurrogateError return str().join(decoded) def encodefilename(fn: str) -> bytes: if FS_ENCODING == "ascii": # ASCII encoder of Python 2 expects that the error handler returns a # Unicode string encodable to ASCII, whereas our surrogateescape error # handler has to return bytes in 0x80-0xFF range. encoded = [] for index, ch in enumerate(fn): code = ord(ch) if code < 128: ch = bytes_chr(code) elif 0xDC80 <= code <= 0xDCFF: ch = bytes_chr(code - 0xDC00) else: raise UnicodeEncodeError( FS_ENCODING, fn, index, index + 1, "ordinal not in range(128)" ) encoded.append(ch) return bytes().join(encoded) elif FS_ENCODING == "utf-8": # UTF-8 encoder of Python 2 encodes surrogates, so U+DC80-U+DCFF # doesn't go through our error handler encoded = [] for index, ch in enumerate(fn): code = ord(ch) if 0xD800 <= code <= 0xDFFF: if 0xDC80 <= code <= 0xDCFF: ch = bytes_chr(code - 0xDC00) encoded.append(ch) else: raise UnicodeEncodeError( FS_ENCODING, fn, index, index + 1, "surrogates not allowed" ) else: ch_utf8 = ch.encode("utf-8") encoded.append(ch_utf8) return bytes().join(encoded) else: return fn.encode(FS_ENCODING, FS_ERRORS) def decodefilename(fn: bytes) -> str: return fn.decode(FS_ENCODING, FS_ERRORS) FS_ENCODING = sys.getfilesystemencoding() if WINDOWS and not PY3: # py2 thinks win* is mbcs, probably a bug? anyways this works FS_ENCODING = "utf-8" # normalize the filesystem encoding name. # For example, we expect "utf-8", not "UTF8". FS_ENCODING = codecs.lookup(FS_ENCODING).name def register_surrogateescape() -> None: """ Registers the surrogateescape error handler on Python 2 (only) """ if PY3: return try: codecs.lookup_error(FS_ERRORS) except LookupError: codecs.register_error(FS_ERRORS, surrogateescape_handler) ================================================ FILE: copyparty/sutil.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os import tempfile from datetime import datetime from .__init__ import CORES from .authsrv import VFS, AuthSrv from .bos import bos from .th_cli import ThumbCli from .th_srv import TH_CH from .util import UTC, vjoin, vol_san if True: # pylint: disable=using-constant-test from typing import Any, Generator, Optional from .util import NamedLogger TAR_NO_OPUS = set("aac|m4a|m4b|m4r|mp3|oga|ogg|opus|wma".split("|")) class StreamArc(object): def __init__( self, log: "NamedLogger", asrv: AuthSrv, fgen: Generator[dict[str, Any], None, None], **kwargs: Any ): self.log = log self.asrv = asrv self.args = asrv.args self.fgen = fgen self.stopped = False def gen(self) -> Generator[Optional[bytes], None, None]: raise Exception("override me") def stop(self) -> None: self.stopped = True def gfilter( fgen: Generator[dict[str, Any], None, None], thumbcli: ThumbCli, uname: str, vtop: str, fmt: str, ) -> Generator[dict[str, Any], None, None]: from concurrent.futures import ThreadPoolExecutor pend = [] with ThreadPoolExecutor(max_workers=CORES) as tp: try: for f in fgen: task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt) pend.append((task, f)) if pend[0][0].done() or len(pend) > CORES * 4: task, f = pend.pop(0) try: f = task.result(600) except: pass yield f for task, f in pend: try: f = task.result(600) except: pass yield f except Exception as ex: thumbcli.log("gfilter flushing ({})".format(ex)) for task, f in pend: try: task.result(600) except: pass thumbcli.log("gfilter flushed") def enthumb( thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str ) -> dict[str, Any]: rem = f["vp"] ext = rem.rsplit(".", 1)[-1].lower() if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS): raise Exception() vp = vjoin(vtop, rem.split("/", 1)[1]) vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False) dbv, vrem = vn.get_dbv(rem) thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt) if not thp: raise Exception() ext = fmt if fmt == "wav" else TH_CH.get(fmt[:1], fmt) sz = bos.path.getsize(thp) st: os.stat_result = f["st"] ts = st.st_mtime f["ap"] = thp f["vp"] = f["vp"].rsplit(".", 1)[0] + "." + ext f["st"] = os.stat_result((st.st_mode, -1, -1, 1, 1000, 1000, sz, ts, ts, ts)) return f def errdesc( vfs: VFS, errors: list[tuple[str, str]] ) -> tuple[dict[str, Any], list[str]]: report = ["copyparty failed to add the following files to the archive:", ""] for fn, err in errors: report.extend([" file: %r" % (fn,), "error: %s" % (err,), ""]) btxt = "\r\n".join(report).encode("utf-8", "replace") btxt = vol_san(list(vfs.all_vols.values()), btxt) with tempfile.NamedTemporaryFile(prefix="copyparty-", delete=False) as tf: tf_path = tf.name tf.write(btxt) dt = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S") bos.chmod(tf_path, 0o444) return { "vp": "archive-errors-{}.txt".format(dt), "ap": tf_path, "st": bos.stat(tf_path), }, report ================================================ FILE: copyparty/svchub.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import atexit import errno import json import logging import os import re import shlex import signal import socket import string import sys import threading import time from datetime import datetime # from inspect import currentframe # print(currentframe().f_lineno) if True: # pylint: disable=using-constant-test from types import FrameType import typing from typing import Any, Optional, Union from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode from .__version__ import S_VERSION, VERSION from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who from .bos import bos from .cert import ensure_cert from .fsutil import ramdisk_chk from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN from .pwhash import HAVE_ARGON2 from .tcpsrv import TcpSrv from .th_srv import ( H_PIL_AVIF, H_PIL_HEIF, H_PIL_WEBP, HAVE_FFMPEG, HAVE_FFPROBE, HAVE_PIL, HAVE_RAW, HAVE_VIPS, ThumbSrv, ) from .up2k import Up2k from .util import ( DEF_EXP, DEF_MTE, DEF_MTH, FFMPEG_URL, HAVE_PSUTIL, HAVE_SQLITE3, HAVE_ZMQ, RE_ANSI, URL_BUG, UTC, VERSIONS, Daemon, Garda, HLog, HMaccas, ODict, alltrace, build_netmap, expat_ver, gzip, html_escape, load_ipr, load_ipu, lock_file, min_ex, mp, odfusion, pybin, read_utf8, start_log_thrs, start_stackmon, termsize, ub64enc, umktrans, ) if HAVE_SQLITE3: import sqlite3 if TYPE_CHECKING: try: from .mdns import MDNS from .ssdp import SSDPd except: pass if PY2: range = xrange # type: ignore if PY2: from urllib2 import Request, urlopen else: from urllib.request import Request, urlopen VER_IDP_DB = 1 VER_SESSION_DB = 1 VER_SHARES_DB = 2 class SvcHub(object): """ Hosts all services which cannot be parallelized due to reliance on monolithic resources. Creates a Broker which does most of the heavy stuff; hosted services can use this to perform work: hub.broker.(destination, args_list). Either BrokerThr (plain threads) or BrokerMP (multiprocessing) is used depending on configuration. Nothing is returned synchronously; if you want any value returned from the call, put() can return a queue (if want_reply=True) which has a blocking get() with the response. """ def __init__( self, args: argparse.Namespace, dargs: argparse.Namespace, argv: list[str], printed: str, ) -> None: self.args = args self.dargs = dargs self.argv = argv self.E: EnvParams = args.E self.no_ansi = args.no_ansi self.flo = args.flo self.tz = UTC if args.log_utc else None self.logf: Optional[typing.TextIO] = None self.logf_base_fn = "" self.is_dut = False # running in unittest; always False self.stop_req = False self.stopping = False self.stopped = False self.reload_req = False self.reload_mutex = threading.Lock() self.stop_cond = threading.Condition() self.nsigs = 3 self.retcode = 0 self.httpsrv_up = 0 self.qr_tsz = None self.log_mutex = threading.Lock() self.cday = 0 self.cmon = 0 self.tstack = 0.0 self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) if args.sss or args.s >= 3: args.ss = True args.no_dav = True args.no_logues = True args.no_readme = True args.lo = args.lo or "cpp-%Y-%m%d-%H%M%S.txt.xz" args.ls = args.ls or "**,*,ln,p,r" if args.ss or args.s >= 2: args.s = True args.unpost = 0 args.no_del = True args.no_mv = True args.reflink = True args.dav_auth = True args.vague_403 = True args.nih = True if args.s: args.dotpart = True args.no_thumb = True args.no_mtag_ff = True args.no_robots = True args.force_js = True if not self._process_config(): raise Exception(BAD_CFG) # for non-http clients (ftp, tftp) self.bans: dict[str, int] = {} self.gpwd = Garda(self.args.ban_pw) self.gpwc = Garda(self.args.ban_pwc) self.g404 = Garda(self.args.ban_404) self.g403 = Garda(self.args.ban_403) self.g422 = Garda(self.args.ban_422, False) self.gmal = Garda(self.args.ban_422) self.gurl = Garda(self.args.ban_url) self.log_div = 10 ** (6 - args.log_tdec) self.log_efmt = "%02d:%02d:%02d.%0{}d".format(args.log_tdec) self.log_dfmt = "%04d-%04d-%06d.%0{}d".format(args.log_tdec) if args.q: self.log = self._log_disabled elif args.lo and args.flo == 2 and not self.no_ansi: self.log = self._log_en_f2 else: self.log = self._log_enabled if args.lo: self._setup_logfile(printed) lg = logging.getLogger() lh = HLog(self.log) lg.handlers = [lh] lg.setLevel(logging.DEBUG) self._check_env() if args.stackmon: start_stackmon(args.stackmon, 0) if args.log_thrs: start_log_thrs(self.log, args.log_thrs, 0) if not args.use_fpool and args.j != 1: args.no_fpool = True t = "multithreading enabled with -j {}, so disabling fpool -- this can reduce upload performance on some filesystems, and make some antivirus-softwares " c = 0 if ANYWIN: t += "(especially Microsoft Defender) stress your CPU and HDD severely during big uploads" c = 3 else: t += "consume more resources (CPU/HDD) than normal" self.log("root", t.format(args.j), c) if not args.no_fpool and args.j != 1: t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled" self.log("root", t.format(args.j), c=3) args.no_fpool = True args.p_nodav = [int(x.strip()) for x in args.p_nodav.split(",") if x] if args.dav_port and args.dav_port not in args.p: args.p.append(args.dav_port) for name, arg in ( ("iobuf", "iobuf"), ("s-rd-sz", "s_rd_sz"), ("s-wr-sz", "s_wr_sz"), ): zi = getattr(args, arg) if zi < 32768: t = "WARNING: expect very poor performance because you specified a very low value (%d) for --%s" self.log("root", t % (zi, name), 3) zi = 2 zi2 = 2 ** (zi - 1).bit_length() if zi != zi2: zi3 = 2 ** ((zi - 1).bit_length() - 1) t = "WARNING: expect poor performance because --%s is not a power-of-two; consider using %d or %d instead of %d" self.log("root", t % (name, zi2, zi3, zi), 3) if args.s_rd_sz > args.iobuf: t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance" self.log("root", t % (args.s_rd_sz, args.iobuf), 3) zs = "" if args.th_ram_max < 0.22: zs = "generate thumbnails" elif args.th_ram_max < 1: zs = "generate audio waveforms or spectrograms" if zs: t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s" self.log("root", t % (args.th_ram_max, zs), 3) if args.chpw and args.have_idp_hdrs and "pw" not in args.auth_ord.split(","): t = "ERROR: user-changeable passwords is not compatible with your current configuration. Choose one of these options to fix it:\n option1: disable --chpw\n option2: remove all use of IdP features; --idp-*\n option3: change --auth-ord to something like pw,idp,ipu" self.log("root", t, 1) raise Exception(t) noch = set() for zs in args.chpw_no or []: zsl = [x.strip() for x in zs.split(",")] noch.update([x for x in zsl if x]) args.chpw_no = noch if args.ipu: iu, nm = load_ipu(self.log, args.ipu, True) setattr(args, "ipu_iu", iu) setattr(args, "ipu_nm", nm) if args.ipr: ipr = load_ipr(self.log, args.ipr, True) setattr(args, "ipr_u", ipr) for zs in "ah_salt fk_salt dk_salt".split(): if getattr(args, "show_%s" % (zs,)): self.log("root", "effective %s is %s" % (zs, getattr(args, zs))) if args.ah_cli or args.ah_gen: args.idp_store = 0 args.no_ses = True args.shr = "" if args.idp_store and args.have_idp_hdrs: self.setup_db("idp") if not self.args.no_ses: self.setup_db("ses") args.shr1 = "" if args.shr: self.setup_share_db() bri = "zy"[args.theme % 2 :][:1] ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)] args.theme = "{0}{1} {0} {1}".format(ch, bri) if args.no_stack: args.stack_who = "no" if args.nid: args.du_who = "no" args.du_iwho = n_du_who(args.du_who) if args.ver and args.ver_who == "no": args.ver_who = "all" args.ver_iwho = n_ver_who(args.ver_who) if args.nih: args.vname = "" args.doctitle = args.doctitle.replace(" @ --name", "") else: args.vname = args.name args.doctitle = args.doctitle.replace("--name", args.vname) args.bname = args.bname.replace("--name", args.vname) or args.vname for zs in "shr_site up_site".split(): if getattr(args, zs) == "--site": setattr(args, zs, args.site) if args.log_fk: args.log_fk = re.compile(args.log_fk) # initiate all services to manage self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs) ramdisk_chk(self.asrv) if args.cgen: self.asrv.cgen() if args.exit == "cfg": sys.exit(0) if args.ls: self.asrv.dbg_ls() if not ANYWIN: self._setlimits() self.log("root", "max clients: {}".format(self.args.nc)) self.tcpsrv = TcpSrv(self) if not self.tcpsrv.srv and self.args.ign_ebind_all: self.args.no_fastboot = True self.up2k = Up2k(self) self._feature_test() decs = {k.strip(): 1 for k in self.args.th_dec.split(",")} if not HAVE_VIPS: decs.pop("vips", None) if not HAVE_PIL: decs.pop("pil", None) if not HAVE_RAW: decs.pop("raw", None) if not HAVE_FFMPEG or not HAVE_FFPROBE: decs.pop("ff", None) # compressed formats; "s3z=s3m.zip, s3gz=s3m.gz, ..." zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")] args.au_unpk = {x[0]: x[1] for x in zlss} self.args.th_dec = list(decs.keys()) self.thumbsrv = None want_ff = False if not args.no_thumb: t = ", ".join(self.args.th_dec) or "(None available)" self.log("thumb", "decoder preference: {}".format(t)) if self.args.th_dec: self.thumbsrv = ThumbSrv(self) else: want_ff = True msg = "need either Pillow, pyvips, or FFmpeg to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n{0}apt install ffmpeg" msg = msg.format(" " * 37, os.path.basename(pybin)) if EXE: msg = "copyparty.exe cannot use Pillow or pyvips; need ffprobe.exe and ffmpeg.exe to create thumbnails" self.log("thumb", msg, c=3) if not args.no_acode and args.no_thumb: msg = "setting --no-acode because --no-thumb (sorry)" self.log("thumb", msg, c=6) args.no_acode = True if not args.no_acode and (not HAVE_FFMPEG or not HAVE_FFPROBE): msg = "setting --no-acode because either FFmpeg or FFprobe is not available" self.log("thumb", msg, c=6) args.no_acode = True want_ff = True if want_ff and ANYWIN: self.log("thumb", "download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3) if not args.no_acode: if not re.match("^(0|[qv][0-9]|[0-9]{2,3}k)$", args.q_mp3.lower()): t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]" raise Exception(t % (args.q_mp3,)) else: zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(",")) args.au_unpk = { k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss } args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage) zms = "" if not args.https_only: zms += "d" if not args.http_only: zms += "D" if args.sftp: from .sftpd import Sftpd self.sftpd: Optional[Sftpd] = None if args.ftp or args.ftps: from .ftpd import Ftpd self.ftpd: Optional[Ftpd] = None zms += "f" if args.ftp else "F" if args.tftp: from .tftpd import Tftpd self.tftpd: Optional[Tftpd] = None if args.sftp or args.ftp or args.ftps or args.tftp: Daemon(self.start_ftpd, "start_tftpd") if args.smb: # impacket.dcerpc is noisy about listen timeouts sto = socket.getdefaulttimeout() socket.setdefaulttimeout(None) from .smbd import SMB self.smbd = SMB(self) socket.setdefaulttimeout(sto) self.smbd.start() zms += "s" if not args.zms: args.zms = zms self.zc_ngen = 0 self.mdns: Optional["MDNS"] = None self.ssdp: Optional["SSDPd"] = None # decide which worker impl to use if self.check_mp_enable(): from .broker_mp import BrokerMp as Broker else: from .broker_thr import BrokerThr as Broker # type: ignore self.broker = Broker(self) # create netmaps early to avoid firewall gaps, # but the mutex blocks multiprocessing startup for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split(): try: getattr(args, zs).mutex = threading.Lock() except: pass if args.ipr: for nm in args.ipr_u.values(): nm.mutex = threading.Lock() def _db_onfail_ses(self) -> None: self.args.no_ses = True def _db_onfail_idp(self) -> None: self.args.idp_store = 0 def setup_db(self, which: str) -> None: """ the "non-mission-critical" databases; if something looks broken then just nuke it """ if which == "ses": native_ver = VER_SESSION_DB db_path = self.args.ses_db desc = "sessions-db" pathopt = "ses-db" sanchk_q = "select count(*) from us" createfun = self._create_session_db failfun = self._db_onfail_ses elif which == "idp": native_ver = VER_IDP_DB db_path = self.args.idp_db desc = "idp-db" pathopt = "idp-db" sanchk_q = "select count(*) from us" createfun = self._create_idp_db failfun = self._db_onfail_idp else: raise Exception("unknown cachetype") if not db_path.endswith(".db"): zs = "config option --%s (the %s) was configured to [%s] which is invalid; must be a filepath ending with .db" self.log("root", zs % (pathopt, desc, db_path), 1) raise Exception(BAD_CFG) if not HAVE_SQLITE3: failfun() if which == "ses": zs = "disabling sessions, will use plaintext passwords in cookies" elif which == "idp": zs = "disabling idp-db, will be unable to remember IdP-volumes after a restart" self.log("root", "WARNING: sqlite3 not available; %s" % (zs,), 3) return assert sqlite3 # type: ignore # !rm db_lock = db_path + ".lock" try: create = not os.path.getsize(db_path) except: create = True zs = "creating new" if create else "opening" self.log("root", "%s %s %s" % (zs, desc, db_path)) for tries in range(2): sver = 0 try: db = sqlite3.connect(db_path) cur = db.cursor() try: zs = "select v from kv where k='sver'" sver = cur.execute(zs).fetchall()[0][0] if sver > native_ver: zs = "this version of copyparty only understands %s v%d and older; the db is v%d" raise Exception(zs % (desc, native_ver, sver)) cur.execute(sanchk_q).fetchone() except: if sver: raise sver = createfun(cur) err = self._verify_db( cur, which, pathopt, db_path, desc, sver, native_ver ) if err: tries = 99 self.args.no_ses = True self.log("root", err, 3) break except Exception as ex: if tries or sver > native_ver: raise t = "%s is unusable; deleting and recreating: %r" self.log("root", t % (desc, ex), 3) try: cur.close() # type: ignore except: pass try: db.close() # type: ignore except: pass try: os.unlink(db_lock) except: pass os.unlink(db_path) def _create_session_db(self, cur: "sqlite3.Cursor") -> int: sch = [ r"create table kv (k text, v int)", r"create table us (un text, si text, t0 int)", # username, session-id, creation-time r"create index us_un on us(un)", r"create index us_si on us(si)", r"create index us_t0 on us(t0)", r"insert into kv values ('sver', 1)", ] for cmd in sch: cur.execute(cmd) self.log("root", "created new sessions-db") return 1 def _create_idp_db(self, cur: "sqlite3.Cursor") -> int: sch = [ r"create table kv (k text, v int)", r"create table us (un text, gs text)", # username, groups r"create index us_un on us(un)", r"insert into kv values ('sver', 1)", ] for cmd in sch: cur.execute(cmd) self.log("root", "created new idp-db") return 1 def _verify_db( self, cur: "sqlite3.Cursor", which: str, pathopt: str, db_path: str, desc: str, sver: int, native_ver: int, ) -> str: # ensure writable (maybe owned by other user) db = cur.connection try: zil = cur.execute("select v from kv where k='pid'").fetchall() if len(zil) > 1: raise Exception() owner = zil[0][0] except: owner = 0 if which == "ses": cons = "Will now disable sessions and instead use plaintext passwords in cookies." elif which == "idp": cons = "Each IdP-volume will not become available until its associated user sends their first request." else: raise Exception() if not lock_file(db_path + ".lock"): t = "the %s [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --%s or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. %s" return t % (desc, db_path, owner, pathopt, cons) vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000))) if owner: # wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720 for k, v in vars: cur.execute("update kv set v=? where k=?", (v, k)) else: # wear-estimate: 3~4 cells; offsets 0x10, 0x50, 0x19180, 0x19710, 0x36000, 0x360b0, 0x36b90 for k, v in vars: cur.execute("insert into kv values(?, ?)", (k, v)) if sver < native_ver: cur.execute("delete from kv where k='sver'") cur.execute("insert into kv values('sver',?)", (native_ver,)) db.commit() cur.close() db.close() return "" def setup_share_db(self) -> None: al = self.args if not HAVE_SQLITE3: self.log("root", "sqlite3 not available; disabling --shr", 1) al.shr = "" return assert sqlite3 # type: ignore # !rm al.shr = al.shr.strip("/") if "/" in al.shr or not al.shr: t = "config error: --shr must be the name of a virtual toplevel directory to put shares inside" self.log("root", t, 1) raise Exception(t) al.shr = "/%s/" % (al.shr,) al.shr1 = al.shr[1:] # policy: # the shares-db is important, so panic if something is wrong db_path = self.args.shr_db db_lock = db_path + ".lock" try: create = not os.path.getsize(db_path) except: create = True zs = "creating new" if create else "opening" self.log("root", "%s shares-db %s" % (zs, db_path)) sver = 0 try: db = sqlite3.connect(db_path) cur = db.cursor() if not create: zs = "select v from kv where k='sver'" sver = cur.execute(zs).fetchall()[0][0] if sver > VER_SHARES_DB: zs = "this version of copyparty only understands shares-db v%d and older; the db is v%d" raise Exception(zs % (VER_SHARES_DB, sver)) cur.execute("select count(*) from sh").fetchone() except Exception as ex: t = "could not open shares-db; will now panic...\nthe following database must be repaired or deleted before you can launch copyparty:\n%s\n\nERROR: %s\n\nadditional details:\n%s\n" self.log("root", t % (db_path, ex, min_ex()), 1) raise try: zil = cur.execute("select v from kv where k='pid'").fetchall() if len(zil) > 1: raise Exception() owner = zil[0][0] except: owner = 0 if not lock_file(db_lock): t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic." t = t % (db_path, owner) self.log("root", t, 1) raise Exception(t) sch1 = [ r"create table kv (k text, v int)", r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)", # sharekey, password, src, perms, numFiles, owner, created, expires ] sch2 = [ r"create table sf (k text, vp text)", r"create index sf_k on sf(k)", r"create index sh_k on sh(k)", r"create index sh_t1 on sh(t1)", r"insert into kv values ('sver', 2)", ] assert db # type: ignore # !rm assert cur # type: ignore # !rm if not sver: sver = VER_SHARES_DB for cmd in sch1 + sch2: cur.execute(cmd) self.log("root", "created new shares-db") if sver == 1: for cmd in sch2: cur.execute(cmd) cur.execute("update sh set st = 0") self.log("root", "shares-db schema upgrade ok") if sver < VER_SHARES_DB: cur.execute("delete from kv where k='sver'") cur.execute("insert into kv values('sver',?)", (VER_SHARES_DB,)) vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000))) if owner: # wear-estimate: same as sessions-db for k, v in vars: cur.execute("update kv set v=? where k=?", (v, k)) else: for k, v in vars: cur.execute("insert into kv values(?, ?)", (k, v)) db.commit() cur.close() db.close() def start_ftpd(self) -> None: time.sleep(30) if hasattr(self, "sftpd") and not self.sftpd: self.restart_sftpd() if hasattr(self, "ftpd") and not self.ftpd: self.restart_ftpd() if hasattr(self, "tftpd") and not self.tftpd: self.restart_tftpd() def restart_sftpd(self) -> None: if not hasattr(self, "sftpd"): return from .sftpd import Sftpd if self.sftpd: return # todo self.sftpd = Sftpd(self) self.sftpd.run() self.log("root", "started SFTPd") def restart_ftpd(self) -> None: if not hasattr(self, "ftpd"): return from .ftpd import Ftpd if self.ftpd: return # todo if not os.path.exists(self.args.cert): ensure_cert(self.log, self.args) self.ftpd = Ftpd(self) self.log("root", "started FTPd") def restart_tftpd(self) -> None: if not hasattr(self, "tftpd"): return from .tftpd import Tftpd if self.tftpd: return # todo self.tftpd = Tftpd(self) def thr_httpsrv_up(self) -> None: time.sleep(1 if self.args.ign_ebind_all else 5) expected = self.broker.num_workers * self.tcpsrv.nsrv failed = expected - self.httpsrv_up if not failed: return if self.args.ign_ebind_all: if not self.tcpsrv.srv: for _ in range(self.broker.num_workers): self.broker.say("cb_httpsrv_up") return if self.args.ign_ebind and self.tcpsrv.srv: return t = "{}/{} workers failed to start" t = t.format(failed, expected) self.log("root", t, 1) self.retcode = 1 self.sigterm() def sigterm(self) -> None: self.signal_handler(signal.SIGTERM, None) def sticky_qr(self) -> None: self._sticky_qr() def _unsticky_qr(self, flush=True) -> None: print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="") if flush: sys.stderr.flush() def _sticky_qr(self, force: bool = False) -> None: sz = termsize() if self.qr_tsz == sz: if not force: return else: force = False if self.qr_tsz: self._unsticky_qr(False) else: atexit.register(self._unsticky_qr) tw, th = self.qr_tsz = sz zs1, qr = self.tcpsrv.qr.split("\n", 1) url, colr = zs1.split(" ", 1) nl = len(qr.split("\n")) # numlines lp = 3 if nl * 2 + 4 < tw else 0 # leftpad lp0 = lp if self.args.qr_pin == 2: url = "" else: while lp and (nl + lp) * 2 + len(url) + 1 > tw: lp -= 1 if (nl + lp) * 2 + len(url) + 1 > tw: qr = url + "\n" + qr url = "" nl += 1 lp = lp0 sh = 1 + th - nl if lp: zs = " " * lp qr = zs + qr.replace("\n", "\n" + zs) if url: url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url) qr = colr + qr t = "%s\033[%dA" % ("\n" * nl, nl) t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url) if not force: self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6) self.pr(t, file=sys.stderr, end="") def _qr_thr(self): qr = self.tcpsrv.qr w8 = self.args.qr_wait if w8: time.sleep(w8) self.log("qr-code", qr) if self.args.qr_stdout: self.pr(self.tcpsrv.qr) if self.args.qr_stderr: self.pr(self.tcpsrv.qr, file=sys.stderr) w8 = self.args.qr_every msg = "%s\033[%dA" % (qr, len(qr.split("\n"))) while w8: time.sleep(w8) if self.stopping: break if self.args.qr_pin: self._sticky_qr(True) else: self.log("qr-code", msg) w8 = self.args.qr_winch while w8: time.sleep(w8) if self.stopping: break self._sticky_qr() def cb_httpsrv_up(self) -> None: self.httpsrv_up += 1 if self.httpsrv_up != self.broker.num_workers: return ar = self.args for _ in range(10 if ar.sftp or ar.ftp or ar.ftps else 0): time.sleep(0.03) if self.ftpd if ar.ftp or ar.ftps else ar.sftp: break if self.tcpsrv.qr: if self.args.qr_pin: self.sticky_qr() if self.args.qr_wait or self.args.qr_every or self.args.qr_winch: Daemon(self._qr_thr, "qr") else: if not self.args.qr_pin: self.log("qr-code", self.tcpsrv.qr) if self.args.qr_stdout: self.pr(self.tcpsrv.qr) if self.args.qr_stderr: self.pr(self.tcpsrv.qr, file=sys.stderr) else: self.log("root", "workers OK\n") self.after_httpsrv_up() def after_httpsrv_up(self) -> None: self.up2k.init_vols() Daemon(self.sd_notify, "sd-notify") def _feature_test(self) -> None: fok = [] fng = [] t_ff = "transcode audio, create spectrograms, video thumbnails" to_check = [ (HAVE_SQLITE3, "sqlite", "sessions and file/media indexing"), (HAVE_PIL, "pillow", "image thumbnails (plenty fast)"), (HAVE_VIPS, "vips", "image thumbnails (faster, eats more ram)"), (H_PIL_WEBP, "pillow-webp", "create thumbnails as webp files"), (HAVE_FFMPEG, "ffmpeg", t_ff + ", good-but-slow image thumbnails"), (HAVE_FFPROBE, "ffprobe", t_ff + ", read audio/media tags"), (HAVE_MUTAGEN, "mutagen", "read audio tags (ffprobe is better but slower)"), (HAVE_ARGON2, "argon2", "secure password hashing (advanced users only)"), (HAVE_ZMQ, "pyzmq", "send zeromq messages from event-hooks"), (H_PIL_HEIF, "pillow-heif", "read .heif pics with pillow (rarely useful)"), (H_PIL_AVIF, "pillow-avif", "read .avif pics with pillow (rarely useful)"), (HAVE_RAW, "rawpy", "read RAW images"), ] if ANYWIN: to_check += [ (HAVE_PSUTIL, "psutil", "improved plugin cleanup (rarely useful)") ] verbose = self.args.deps if verbose: self.log("dependencies", "") for have, feat, what in to_check: lst = fok if have else fng lst.append((feat, what)) if verbose: zi = 2 if have else 5 sgot = "found" if have else "missing" t = "%7s: %s \033[36m(%s)" self.log("dependencies", t % (sgot, feat, what), zi) if verbose: self.log("dependencies", "") return sok = ", ".join(x[0] for x in fok) sng = ", ".join(x[0] for x in fng) t = "" if sok: t += "OK: \033[32m" + sok if sng: if t: t += ", " t += "\033[0mNG: \033[35m" + sng t += "\033[0m, see --deps (this is fine btw)" self.log("optional-dependencies", t, 6) def _check_env(self) -> None: al = self.args if self.args.no_bauth: t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead" self.log("root", t, 3) if self.args.bauth_last: self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3) have_tcp = False for zs in al.i: if not zs.startswith(("unix:", "fd:")): have_tcp = True if not have_tcp: zb = False zs = "z zm zm4 zm6 zmv zmvv zs zsv zv" for zs in zs.split(): if getattr(al, zs, False): setattr(al, zs, False) zb = True if zb: t = "not listening on any ip-addresses (only unix-sockets and/or FDs); cannot enable zeroconf/mdns/ssdp as requested" self.log("root", t, 3) if not self.args.no_dav: from .dxml import DXML_OK if not DXML_OK: if not self.args.no_dav: self.args.no_dav = True t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n" self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1) if ( not E.scfg and not al.unsafe_state and not os.environ.get("PRTY_UNSAFE_STATE") ): t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1." if not al.no_ses: al.no_ses = True t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!" self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1) if al.idp_store == 1: al.idp_store = 0 self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3) if al.idp_store: t2 = "ERROR: Cannot enable --idp-store %s" % (t,) self.log("root", t2, 1) raise Exception(t2) if al.shr: t2 = "ERROR: Cannot enable shares %s" % (t,) self.log("root", t2, 1) raise Exception(t2) def _process_config(self) -> bool: al = self.args al.zm_on = al.zm_on or al.z_on al.zs_on = al.zs_on or al.z_on al.zm_off = al.zm_off or al.z_off al.zs_off = al.zs_off or al.z_off ns = "zm_on zm_off zs_on zs_off acao acam" for n in ns.split(" "): vs = getattr(al, n).split(",") vs = [x.strip() for x in vs] vs = [x for x in vs if x] setattr(al, n, vs) ns = "acao acam" for n in ns.split(" "): vs = getattr(al, n) vd = {zs: 1 for zs in vs} setattr(al, n, vd) ns = "acao" for n in ns.split(" "): vs = getattr(al, n) vs = [x.lower() for x in vs] setattr(al, n, vs) ns = "ihead ohead" for n in ns.split(" "): vs = getattr(al, n) or [] vs = [x.lower() for x in vs] setattr(al, n, vs) R = al.rp_loc if "//" in R or ":" in R: t = "found URL in --rp-loc; it should be just the location, for example /foo/bar" raise Exception(t) al.R = R = R.strip("/") al.SR = "/" + R if R else "" al.RS = R + "/" if R else "" al.SRS = "/" + R + "/" if R else "/" if al.rsp_jtr: al.rsp_slp = 0.000001 zsl = al.th_covers.split(",") zsl = [x.strip() for x in zsl] zsl = [x for x in zsl if x] al.th_covers = zsl al.th_coversd = zsl + ["." + x for x in zsl] al.th_covers_set = set(al.th_covers) al.th_coversd_set = set(al.th_coversd) for k in "c".split(" "): vl = getattr(al, k) if not vl: continue vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl] setattr(al, k, vl) for k in "lo hist dbpath ssl_log".split(" "): vs = getattr(al, k) if vs: vs = os.path.expandvars(os.path.expanduser(vs)) setattr(al, k, vs) for k in "idp_adm stats_u".split(" "): vs = getattr(al, k) vsa = [x.strip() for x in vs.split(",")] vsa = [x.lower() for x in vsa if x] setattr(al, k + "_set", set(vsa)) for k in "smsg".split(" "): vs = getattr(al, k) vsa = [x.strip() for x in vs.split(",")] vsa = [x.upper() for x in vsa if x] setattr(al, k + "_set", set(vsa)) zs = "dav_ua1 sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip" for k in zs.split(" "): vs = getattr(al, k) if not vs or vs == "no": setattr(al, k, None) else: setattr(al, k, re.compile(vs)) for k in "tftp_lsf".split(" "): vs = getattr(al, k) if not vs or vs == "no": setattr(al, k, None) else: setattr(al, k, re.compile("^" + vs + "$")) if al.banmsg.startswith("@"): with open(al.banmsg[1:], "rb") as f: al.banmsg_b = f.read() else: al.banmsg_b = al.banmsg.encode("utf-8") + b"\n" if not al.sus_urls: al.ban_url = "no" elif al.ban_url == "no": al.sus_urls = None zs = "fika idp_h_grp idp_h_key pw_hdr pw_urlp xf_host xf_proto xf_proto_fb xff_hdr" for k in zs.split(" "): setattr(al, k, str(getattr(al, k)).lower().strip()) al.idp_h_usr = [x.lower() for x in al.idp_h_usr or []] al.idp_hm_usr_p = {} for zs0 in al.idp_hm_usr or []: try: sep = zs0[:1] hn, zs1, zs2 = zs0[1:].split(sep) hn = hn.lower() if hn in al.idp_hm_usr_p: al.idp_hm_usr_p[hn][zs1] = zs2 else: al.idp_hm_usr_p[hn] = {zs1: zs2} except: raise Exception("invalid --idp-hm-usr [%s]" % (zs0,)) zs1 = "" zs2 = "" zs = al.idp_chsub while zs: if zs[:1] != "|": raise Exception("invalid --idp-chsub; expected another | but got " + zs) zs1 += zs[1:2] zs2 += zs[2:3] zs = zs[3:] al.idp_chsub_tr = umktrans(zs1, zs2) al.sftp_ipa_nm = build_netmap(al.sftp_ipa or al.ipa or al.ipar, True) al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa or al.ipar, True) al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa or al.ipar, True) al.sftp_key2u = { "%s %s" % (x[1], x[2]): x[0] for x in [x.split(" ") for x in al.sftp_key or []] } mte = ODict.fromkeys(DEF_MTE.split(","), True) al.mte = odfusion(mte, al.mte) mth = ODict.fromkeys(DEF_MTH.split(","), True) al.mth = odfusion(mth, al.mth) exp = ODict.fromkeys(DEF_EXP.split(" "), True) al.exp_md = odfusion(exp, al.exp_md.replace(" ", ",")) al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ",")) for k in ["no_hash", "no_idx", "og_ua", "srch_excl"]: ptn = getattr(self.args, k) if ptn: setattr(self.args, k, re.compile(ptn)) for k in ["idp_gsep"]: ptn = getattr(self.args, k) if "]" in ptn: ptn = "]" + ptn.replace("]", "") if "[" in ptn: ptn = ptn.replace("[", "") + "[" if "-" in ptn: ptn = ptn.replace("-", "") + "-" ptn = ptn.replace("\\", "\\\\").replace("^", "\\^") setattr(self.args, k, re.compile("[%s]" % (ptn,))) try: zf1, zf2 = self.args.rm_retry.split("/") self.args.rm_re_t = float(zf1) self.args.rm_re_r = float(zf2) except: raise Exception("invalid --rm-retry [%s]" % (self.args.rm_retry,)) try: zf1, zf2 = self.args.mv_retry.split("/") self.args.mv_re_t = float(zf1) self.args.mv_re_r = float(zf2) except: raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,)) if self.args.vc_url: zi = max(1, int(self.args.vc_age)) if zi < 3 and "api.copyparty.eu" in self.args.vc_url: zi = 3 self.log("root", "vc-age too low for copyparty.eu; will use 3 hours") self.args.vc_age = zi al.js_utc = "false" if al.localtime else "true" al.tcolor = al.tcolor.lstrip("#") if len(al.tcolor) == 3: # fc5 => ffcc55 al.tcolor = "".join([x * 2 for x in al.tcolor]) if self.args.name_url: zs = html_escape(self.args.name_url, True, True) zs = '%s' % (zs, self.args.name) else: zs = self.args.name self.args.name_html = zs zs = al.u2sz zsl = [x.strip() for x in zs.split(",")] if len(zsl) not in (1, 3): t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)" raise Exception(t) if len(zsl) < 3: zsl = ["1", zs, zs] zi2 = 1 for zs in zsl: zi = int(zs) # arbitrary constraint (anything above 2 GiB is probably unintended) if zi < 1 or zi > 2047: raise Exception("invalid --u2sz; minimum is 1, max is 2047") if zi < zi2: raise Exception("invalid --u2sz; values must be equal or ascending") zi2 = zi al.u2sz = ",".join(zsl) derive_args(al) return True def _ipa2re(self, txt) -> Optional[re.Pattern]: if txt in ("any", "0", ""): return None zs = txt.replace(" ", "").replace(".", "\\.").replace(",", "|") return re.compile("^(?:" + zs + ")") def _setlimits(self) -> None: try: import resource soft, hard = [ int(x) if x > 0 else 1024 * 1024 for x in list(resource.getrlimit(resource.RLIMIT_NOFILE)) ] except: self.log("root", "failed to read rlimits from os", 6) return if not soft or not hard: t = "got bogus rlimits from os ({}, {})" self.log("root", t.format(soft, hard), 6) return want = self.args.nc * 4 new_soft = min(hard, want) if new_soft < soft: return # t = "requesting rlimit_nofile({}), have {}" # self.log("root", t.format(new_soft, soft), 6) try: import resource resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard)) soft = new_soft except: t = "rlimit denied; max open files: {}" self.log("root", t.format(soft), 3) return if soft < want: t = "max open files: {} (wanted {} for -nc {})" self.log("root", t.format(soft, want, self.args.nc), 3) self.args.nc = min(self.args.nc, soft // 2) def _logname(self) -> str: dt = datetime.now(self.tz) fn = str(self.args.lo) for fs in "YmdHMS": fs = "%" + fs if fs in fn: fn = fn.replace(fs, dt.strftime(fs)) return fn def _setup_logfile(self, printed: str) -> None: base_fn = fn = sel_fn = self._logname() do_xz = fn.lower().endswith(".xz") if fn != self.args.lo: ctr = 0 # yup this is a race; if started sufficiently concurrently, two # copyparties can grab the same logfile (considered and ignored) while os.path.exists(sel_fn): ctr += 1 sel_fn = "{}.{}".format(fn, ctr) fn = sel_fn try: bos.makedirs(os.path.dirname(fn)) except: pass try: if do_xz: import lzma lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0) self.args.no_logflush = True else: lh = open(fn, "wt", encoding="utf-8", errors="replace") except: import codecs lh = codecs.open(fn, "w", encoding="utf-8", errors="replace") if getattr(self.args, "free_umask", False): os.fchmod(lh.fileno(), 0o644) argv = [pybin] + self.argv if hasattr(shlex, "quote"): argv = [shlex.quote(x) for x in argv] else: argv = ['"{}"'.format(x) for x in argv] msg = "[+] opened logfile [{}]\n".format(fn) printed += msg t = "t0: {:.3f}\nargv: {}\n\n{}" lh.write(t.format(self.E.t0, " ".join(argv), printed)) self.logf = lh self.logf_base_fn = base_fn print(msg, end="") def run(self) -> None: self.tcpsrv.run() if getattr(self.args, "z_chk", 0) and ( getattr(self.args, "zm", False) or getattr(self.args, "zs", False) ): Daemon(self.tcpsrv.netmon, "netmon") Daemon(self.thr_httpsrv_up, "sig-hsrv-up2") if self.args.vc_url: Daemon(self.check_ver, "ver-chk") sigs = [signal.SIGINT, signal.SIGTERM] if not ANYWIN: sigs.append(signal.SIGUSR1) for sig in sigs: signal.signal(sig, self.signal_handler) # macos hangs after shutdown on sigterm with while-sleep, # windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??) # linux is fine with both, # never lucky if ANYWIN: # msys-python probably fine but >msys-python Daemon(self.stop_thr, "svchub-sig") try: while not self.stop_req: time.sleep(1) except: pass self.shutdown() # cant join; eats signals on win10 while not self.stopped: time.sleep(0.1) else: self.stop_thr() def start_zeroconf(self) -> None: self.zc_ngen += 1 if getattr(self.args, "zm", False): try: from .mdns import MDNS if self.mdns: self.mdns.stop(True) self.mdns = MDNS(self, self.zc_ngen) Daemon(self.mdns.run, "mdns") except: self.log("root", "mdns startup failed;\n" + min_ex(), 3) if getattr(self.args, "zs", False): try: from .ssdp import SSDPd if self.ssdp: self.ssdp.stop() self.ssdp = SSDPd(self, self.zc_ngen) Daemon(self.ssdp.run, "ssdp") except: self.log("root", "ssdp startup failed;\n" + min_ex(), 3) def reload(self, rescan_all_vols: bool, up2k: bool) -> str: t = "users, volumes, and volflags have been reloaded" with self.reload_mutex: self.log("root", "reloading config") self.asrv.reload(9 if up2k else 4) ramdisk_chk(self.asrv) if up2k: self.up2k.reload(rescan_all_vols) t += "; volumes are now reinitializing" else: self.log("root", "reload done") t += "\n\nchanges to global options (if any) require a restart of copyparty to take effect" self.broker.reload() return t def _reload_sessions(self) -> None: with self.asrv.mutex: self.asrv.load_sessions(True) self.broker.reload_sessions() def stop_thr(self) -> None: while not self.stop_req: with self.stop_cond: self.stop_cond.wait(9001) if self.reload_req: self.reload_req = False self.reload(True, True) self.shutdown() def kill9(self, delay: float = 0.0) -> None: if delay > 0.01: time.sleep(delay) print("component stuck; issuing sigkill") time.sleep(0.1) if ANYWIN: os.system("taskkill /f /pid {}".format(os.getpid())) else: os.kill(os.getpid(), signal.SIGKILL) def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None: if self.stopping: if self.nsigs <= 0: try: threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start() time.sleep(0.1) except: pass self.kill9() else: self.nsigs -= 1 return if not ANYWIN and sig == signal.SIGUSR1: self.reload_req = True else: self.stop_req = True with self.stop_cond: self.stop_cond.notify_all() def shutdown(self) -> None: if self.stopping: return # start_log_thrs(print, 0.1, 1) self.stopping = True self.stop_req = True with self.stop_cond: self.stop_cond.notify_all() ret = 1 try: self.pr("OPYTHAT") tasks = [] slp = 0.0 if self.mdns: tasks.append(Daemon(self.mdns.stop, "mdns")) slp = time.time() + 0.5 if self.ssdp: tasks.append(Daemon(self.ssdp.stop, "ssdp")) slp = time.time() + 0.5 self.broker.shutdown() self.tcpsrv.shutdown() self.up2k.shutdown() if hasattr(self, "smbd"): slp = max(slp, time.time() + 0.5) tasks.append(Daemon(self.smbd.stop, "smbd")) if self.thumbsrv: self.thumbsrv.shutdown() for n in range(200): # 10s time.sleep(0.05) if self.thumbsrv.stopped(): break if n == 3: self.log("root", "waiting for thumbsrv (10sec)...") if hasattr(self, "smbd"): zf = max(time.time() - slp, 0) Daemon(self.kill9, a=(zf + 0.5,)) while time.time() < slp: if not next((x for x in tasks if x.is_alive), None): break time.sleep(0.05) self.log("root", "nailed it") ret = self.retcode except: self.pr("\033[31m[ error during shutdown ]\n{}\033[0m".format(min_ex())) raise finally: if self.args.wintitle: print("\033]0;\033\\", file=sys.stderr, end="") sys.stderr.flush() self.pr("\033[0m", end="") if self.logf: self.logf.close() self.stopped = True sys.exit(ret) def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: if not self.logf: return with self.log_mutex: dt = datetime.now(self.tz) ts = self.log_dfmt % ( dt.year, dt.month * 100 + dt.day, (dt.hour * 100 + dt.minute) * 100 + dt.second, dt.microsecond // self.log_div, ) if self.flo == 1: fmt = "@%s [%-21s] %s\n" if not c: if "\033" in msg: msg += "\033[0m" elif isinstance(c, int): msg = "\033[3%sm%s\033[0m" % (c, msg) elif "\033" not in c: msg = "\033[%sm%s\033[0m" % (c, msg) else: msg = "%s%s\033[0m" % (c, msg) if "\033" in src: src += "\033[0m" else: if not c: fmt = "@%s LOG [%-21s] %s\n" elif c == 1: fmt = "@%s CRIT [%-21s] %s\n" elif c == 3: fmt = "@%s WARN [%-21s] %s\n" elif c == 6: fmt = "@%s BTW [%-21s] %s\n" else: fmt = "@%s LOG [%-21s] %s\n" if "\033" in src: src = RE_ANSI.sub("", src) if "\033" in msg: msg = RE_ANSI.sub("", msg) self.logf.write(fmt % (ts, src, msg)) if not self.args.no_logflush: self.logf.flush() if dt.day != self.cday or dt.month != self.cmon: self._set_next_day(dt) def _set_next_day(self, dt: datetime) -> None: if self.cday and self.logf and self.logf_base_fn != self._logname(): self.logf.close() self._setup_logfile("") self.cday = dt.day self.cmon = dt.month def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: """handles logging from all components""" with self.log_mutex: dt = datetime.now(self.tz) if dt.day != self.cday or dt.month != self.cmon: if self.args.log_date: zs = dt.strftime(self.args.log_date) self.log_efmt = "%s %s" % (zs, self.log_efmt.split(" ")[-1]) zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n" zs = zs.format(dt.strftime("%Y-%m-%d")) print(zs, end="") self._set_next_day(dt) if self.logf: self.logf.write(zs) if self.no_ansi: if not c: fmt = "%s %-21s LOG: %s\n" elif c == 1: fmt = "%s %-21s CRIT: %s\n" elif c == 3: fmt = "%s %-21s WARN: %s\n" elif c == 6: fmt = "%s %-21s BTW: %s\n" else: fmt = "%s %-21s LOG: %s\n" if "\033" in msg: msg = RE_ANSI.sub("", msg) if "\033" in src: src = RE_ANSI.sub("", src) else: fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" if not c: pass elif isinstance(c, int): msg = "\033[3%sm%s\033[0m" % (c, msg) elif "\033" not in c: msg = "\033[%sm%s\033[0m" % (c, msg) else: msg = "%s%s\033[0m" % (c, msg) ts = self.log_efmt % ( dt.hour, dt.minute, dt.second, dt.microsecond // self.log_div, ) msg = fmt % (ts, src, msg) try: print(msg, end="") except UnicodeEncodeError: try: print(msg.encode("utf-8", "replace").decode(), end="") except: print(msg.encode("ascii", "replace").decode(), end="") except OSError as ex: if ex.errno != errno.EPIPE: raise if self.logf: self.logf.write(msg) if not self.args.no_logflush: self.logf.flush() def _log_en_f2(self, src: str, msg: str, c: Union[int, str] = 0) -> None: with self.log_mutex: dt = datetime.now(self.tz) if dt.day != self.cday or dt.month != self.cmon: if self.args.log_date: zs = dt.strftime(self.args.log_date) self.log_efmt = "%s %s" % (zs, self.log_efmt.split(" ")[-1]) zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n" zs = zs.format(dt.strftime("%Y-%m-%d")) print(zs, end="") self._set_next_day(dt) if self.logf: self.logf.write(zs) ts = self.log_efmt % ( dt.hour, dt.minute, dt.second, dt.microsecond // self.log_div, ) # logfile: if not c: fmt = "%s %-21s LOG: %s\n" elif c == 1: fmt = "%s %-21s CRIT: %s\n" elif c == 3: fmt = "%s %-21s WARN: %s\n" elif c == 6: fmt = "%s %-21s BTW: %s\n" else: fmt = "%s %-21s LOG: %s\n" fsrc = RE_ANSI.sub("", src) if "\033" in src else src fmsg = RE_ANSI.sub("", msg) if "\033" in msg else msg fmsg = fmt % (ts, fsrc, fmsg) # stdout ansi: fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" if not c: pass elif isinstance(c, int): msg = "\033[3%sm%s\033[0m" % (c, msg) elif "\033" not in c: msg = "\033[%sm%s\033[0m" % (c, msg) else: msg = "%s%s\033[0m" % (c, msg) msg = fmt % (ts, src, msg) try: print(msg, end="") except UnicodeEncodeError: try: print(msg.encode("utf-8", "replace").decode(), end="") except: print(msg.encode("ascii", "replace").decode(), end="") except OSError as ex: if ex.errno != errno.EPIPE: raise self.logf.write(fmsg) if not self.args.no_logflush: self.logf.flush() def pr(self, *a: Any, **ka: Any) -> None: try: with self.log_mutex: print(*a, **ka) except OSError as ex: if ex.errno != errno.EPIPE: raise def check_mp_support(self) -> str: if MACOS and not os.environ.get("PRTY_FORCE_MP"): return "multiprocessing is wonky on mac osx;" elif sys.version_info < (3, 3): return "need python 3.3 or newer for multiprocessing;" try: x: mp.Queue[tuple[str, str]] = mp.Queue(1) x.put(("foo", "bar")) if x.get()[0] != "foo": raise Exception() except: return "multiprocessing is not supported on your platform;" return "" def check_mp_enable(self) -> bool: if self.args.j == 1: return False try: if mp.cpu_count() <= 1 and not os.environ.get("PRTY_FORCE_MP"): raise Exception() except: self.log("svchub", "only one CPU detected; multiprocessing disabled") return False try: # support vscode debugger (bonus: same behavior as on windows) mp.set_start_method("spawn", True) except AttributeError: # py2.7 probably, anyways dontcare pass err = self.check_mp_support() if not err: return True else: self.log("svchub", err) self.log("svchub", "cannot efficiently use multiple CPU cores") return False def sd_notify(self) -> None: try: zb = os.environ.get("NOTIFY_SOCKET") if not zb: return addr = unicode(zb) if addr.startswith("@"): addr = "\0" + addr[1:] t = "".join(x for x in addr if x in string.printable) self.log("sd_notify", t) sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sck.connect(addr) sck.sendall(b"READY=1") except: self.log("sd_notify", min_ex()) def log_stacks(self) -> None: td = time.time() - self.tstack if td < 300: self.log("stacks", "cooldown {}".format(td)) return self.tstack = time.time() zs = "{}\n{}".format(VERSIONS, alltrace()) zb = zs.encode("utf-8", "replace") zb = gzip.compress(zb) zs = ub64enc(zb).decode("ascii") self.log("stacks", zs) def check_ver(self) -> None: next_chk = 0 # self.args.vc_age = 2 / 60 fpath = os.path.join(self.E.cfg, "vuln_advisory.json") while not self.stopping: now = time.time() if now < next_chk: time.sleep(min(999, next_chk - now)) continue age = 0 jtxt = "" src = "[cache] " try: mtime = os.path.getmtime(fpath) age = time.time() - mtime if age < self.args.vc_age * 3600 - 69: zs, jtxt = read_utf8(None, fpath, True).split("\n", 1) if zs != self.args.vc_url: jtxt = "" except Exception as e: t = "will download advisory because cache-file %r could not be read: %s" self.log("ver-chk", t % (fpath, e), 6) if not jtxt: src = "" age = 0 try: req = Request(self.args.vc_url) with urlopen(req, timeout=30) as f: jtxt = f.read().decode("utf-8") try: with open(fpath, "wb") as f: zs = self.args.vc_url + "\n" + jtxt f.write(zs.encode("utf-8")) except Exception as e: t = "failed to write advisory to cache; %s" self.log("ver-chk", t % (e,), 3) except Exception as e: t = "failed to fetch vulnerability advisory; %s" self.log("ver-chk", t % (e,), 1) next_chk = time.time() + 699 if not jtxt: continue try: advisories = json.loads(jtxt) for adv in advisories: if adv.get("state") == "closed": continue vuln = {} for x in adv["vulnerabilities"]: if x["package"]["name"].lower() == "copyparty": vuln = x break if not vuln: continue sver = vuln["patched_versions"].strip(".v") tver = tuple([int(x) for x in sver.split(".")]) if VERSION < tver: zs = json.dumps(adv, indent=2) t = "your version (%s) has a vulnerability! please upgrade:\n%s" self.log("ver-chk", t % (S_VERSION, zs), 1) self.broker.say("httpsrv.set_bad_ver") if self.args.vc_exit: self.sigterm() return else: t = "%sok; v%s and newer is safe" self.log("ver-chk", t % (src, sver), 2) next_chk = time.time() + self.args.vc_age * 3600 - age except Exception as e: t = "failed to process vulnerability advisory; %s" self.log("ver-chk", t % (min_ex()), 1) try: os.unlink(fpath) except: pass ================================================ FILE: copyparty/szip.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import calendar import stat import time from .authsrv import AuthSrv from .bos import bos from .sutil import StreamArc, errdesc from .util import VPTL_WIN, min_ex, sanitize_to, spack, sunpack, yieldfile, zlib if True: # pylint: disable=using-constant-test from typing import Any, Generator, Optional from .util import NamedLogger def dostime2unix(buf: bytes) -> int: t, d = sunpack(b"> 5) & 0x3F th = t >> 11 dd = d & 0x1F dm = (d >> 5) & 0xF dy = (d >> 9) + 1980 tt = (dy, dm, dd, th, tm, ts) tf = "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}" iso = tf.format(*tt) dt = time.strptime(iso, "%Y-%m-%d %H:%M:%S") return int(calendar.timegm(dt)) def unixtime2dos(ts: int) -> bytes: dy, dm, dd, th, tm, ts, _, _, _ = time.gmtime(ts + 1) bd = ((dy - 1980) << 9) + (dm << 5) + dd bt = (th << 11) + (tm << 5) + ts // 2 try: return spack(b" bytes: ret = b"\x50\x4b\x07\x08" fmt = b" bytes: """ does regular file headers and the central directory meme if h_pos is set (h_pos = absolute position of the regular header) """ # appnote 4.5 / zip 3.0 (2008) / unzip 6.0 (2009) says to add z64 # extinfo for values which exceed H, but that becomes an off-by-one # (can't tell if it was clamped or exactly maxval), make it obvious z64v = [sz, sz] if z64 else [] if h_pos and h_pos >= 0xFFFFFFFF: # central, also consider ptr to original header z64v.append(h_pos) # confusingly this doesn't bump if h_pos req_ver = b"\x2d\x00" if z64 else b"\x0a\x00" if icrc32: crc32 = spack(b" tuple[bytes, bool]: """ summary of all file headers, usually the zipfile footer unless something clamps """ ret = b"\x50\x4b\x05\x06" # 2b ndisk, 2b disk0 ret += b"\x00" * 4 cdir_sz = cdir_end - cdir_pos nitems = min(0xFFFF, len(items)) csz = min(0xFFFFFFFF, cdir_sz) cpos = min(0xFFFFFFFF, cdir_pos) need_64 = nitems == 0xFFFF or 0xFFFFFFFF in [csz, cpos] # 2b tnfiles, 2b dnfiles, 4b dir sz, 4b dir pos ret += spack(b" bytes: """ z64 end of central directory added when numfiles or a headerptr clamps """ ret = b"\x50\x4b\x06\x06" # 8b own length from hereon ret += b"\x2c" + b"\x00" * 7 # 2b spec-ver, 2b min-ver ret += b"\x1e\x03\x2d\x00" # 4b ndisk, 4b disk0 ret += b"\x00" * 8 # 8b tnfiles, 8b dnfiles, 8b dir sz, 8b dir pos cdir_sz = cdir_end - cdir_pos ret += spack(b" bytes: """ z64 end of central directory locator points to ecdr64 why """ ret = b"\x50\x4b\x06\x07" # 4b cdisk, 8b start of ecdr64, 4b ndisks ret += spack(b" None: super(StreamZip, self).__init__(log, asrv, fgen) self.utf8 = utf8 self.pre_crc = pre_crc self.pos = 0 self.items: list[tuple[str, int, int, int, int]] = [] def _ct(self, buf: bytes) -> bytes: self.pos += len(buf) return buf def ser(self, f: dict[str, Any]) -> Generator[bytes, None, None]: name = f["vp"] src = f["ap"] st = f["st"] if stat.S_ISDIR(st.st_mode): return sz = st.st_size ts = st.st_mtime h_pos = self.pos crc = 0 if self.pre_crc: for buf in yieldfile(src, self.args.iobuf): crc = zlib.crc32(buf, crc) crc &= 0xFFFFFFFF # some unzip-programs expect a 64bit data-descriptor # even if the only 32bit-exceeding value is the offset, # so force that by placeholdering the filesize too z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF buf = gen_hdr(None, z64, name, sz, ts, self.utf8, crc, self.pre_crc) yield self._ct(buf) for buf in yieldfile(src, self.args.iobuf): if not self.pre_crc: crc = zlib.crc32(buf, crc) yield self._ct(buf) crc &= 0xFFFFFFFF self.items.append((name, sz, ts, crc, h_pos)) if z64 or not self.pre_crc: buf = gen_fdesc(sz, crc, z64) yield self._ct(buf) def gen(self) -> Generator[bytes, None, None]: errf: dict[str, Any] = {} errors = [] mbuf = b"" try: for f in self.fgen: if "err" in f: errors.append((f["vp"], f["err"])) continue try: for x in self.ser(f): mbuf += x if len(mbuf) >= 16384: yield mbuf mbuf = b"" except GeneratorExit: raise except: ex = min_ex(5, True).replace("\n", "\n-- ") errors.append((f["vp"], ex)) if mbuf: yield mbuf mbuf = b"" if errors: errf, txt = errdesc(self.asrv.vfs, errors) self.log("\n".join(([repr(errf)] + txt[1:]))) for x in self.ser(errf): yield x cdir_pos = self.pos for name, sz, ts, crc, h_pos in self.items: z64 = h_pos >= 0xFFFFFFFF or sz >= 0xFFFFFFFF buf = gen_hdr(h_pos, z64, name, sz, ts, self.utf8, crc, self.pre_crc) mbuf += self._ct(buf) if len(mbuf) >= 16384: yield mbuf mbuf = b"" cdir_end = self.pos _, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end) if need_64: ecdir64_pos = self.pos buf = gen_ecdr64(self.items, cdir_pos, cdir_end) mbuf += self._ct(buf) buf = gen_ecdr64_loc(ecdir64_pos) mbuf += self._ct(buf) ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end) yield mbuf + self._ct(ecdr) finally: if errf: bos.unlink(errf["ap"]) ================================================ FILE: copyparty/tcpsrv.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import os import re import socket import sys import time from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .cert import gencert from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen from .util import ( E_ACCESS, E_ADDR_IN_USE, E_ADDR_NOT_AVAIL, E_UNREACH, HAVE_IPV6, IP6_LL, IP6ALL, VF_CAREFUL, Netdev, atomic_move, get_adapters, min_ex, sunpack, termsize, ) if True: # pylint: disable=using-constant-test from typing import Generator, Optional, Union if TYPE_CHECKING: from .svchub import SvcHub if not hasattr(socket, "AF_UNIX"): setattr(socket, "AF_UNIX", -9001) if not hasattr(socket, "IPPROTO_IPV6"): setattr(socket, "IPPROTO_IPV6", 41) if not hasattr(socket, "IP_FREEBIND"): setattr(socket, "IP_FREEBIND", 15) class TcpSrv(object): """ tcplistener which forwards clients to Hub which then uses the least busy HttpSrv to handle it """ def __init__(self, hub: "SvcHub"): self.hub = hub self.args = hub.args self.log = hub.log # mp-safe since issue6056 socket.setdefaulttimeout(120) self.stopping = False self.srv: list[socket.socket] = [] self.bound: list[tuple[str, int]] = [] self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only self.netdevs: dict[str, Netdev] = {} self.netlist = "" self.nsrv = 0 self.qr = "" pad = False ok: dict[str, list[int]] = {} for ip in self.args.i: if ip == "::": if socket.has_ipv6: ips = ["::", "0.0.0.0"] dual = True else: ips = ["0.0.0.0"] dual = False else: ips = [ip] dual = False for ipa in ips: ok[ipa] = [] for port in self.args.p: successful_binds = 0 try: for ipa in ips: try: self._listen(ipa, port) ok[ipa].append(port) successful_binds += 1 except: if dual and ":" in ipa: t = "listen on IPv6 [{}] failed; trying IPv4 {}...\n{}" self.log("tcpsrv", t.format(ipa, ips[1], min_ex()), 3) pad = True continue # binding 0.0.0.0 after :: fails on dualstack # but is necessary on non-dualstack if successful_binds: continue raise except Exception as ex: if self.args.ign_ebind or self.args.ign_ebind_all: t = "could not listen on {}:{}: {}" self.log("tcpsrv", t.format(ip, port, ex), c=3) pad = True else: raise if not self.srv and not self.args.ign_ebind_all: raise Exception("could not listen on any of the given interfaces") if pad: self.log("tcpsrv", "") eps = { "127.0.0.1": Netdev("127.0.0.1", 0, "", "local only"), } if HAVE_IPV6: eps["::1"] = Netdev("::1", 0, "", "local only") nonlocals = [x for x in self.args.i if x not in [k.split("/")[0] for k in eps]] if nonlocals: try: self.netdevs = self.detect_interfaces(self.args.i) except: t = "failed to discover server IP addresses\n" self.log("tcpsrv", t + min_ex(), 3) self.netdevs = {} eps.update({k.split("/")[0]: v for k, v in self.netdevs.items()}) if not eps: for x in nonlocals: eps[x] = Netdev(x, 0, "", "external") else: self.netdevs = {} # keep IPv6 LL-only nics ll_ok: set[str] = set() for ip, nd in self.netdevs.items(): if not ip.startswith(IP6_LL): continue just_ll = True for ip2, nd2 in self.netdevs.items(): if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL): just_ll = False if just_ll or self.args.ll: ll_ok.add(ip.split("/")[0]) listening_on = [] for ip, ports in sorted(ok.items()): for port in sorted(ports): listening_on.append("%s %s" % (ip, port)) qr1: dict[str, list[int]] = {} qr2: dict[str, list[int]] = {} msgs = [] accessible_on = [] title_tab: dict[str, dict[str, int]] = {} title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")] t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)" for ip, desc in sorted(eps.items(), key=lambda x: x[1]): if ip.startswith(IP6_LL) and ip not in ll_ok: continue for port in sorted(self.args.p): if ( port not in ok.get(ip, []) and port not in ok.get("::", []) and port not in ok.get("0.0.0.0", []) ): continue zs = "%s %s" % (ip, port) if zs not in accessible_on: accessible_on.append(zs) proto = " http" if self.args.http_only: pass elif self.args.https_only or port == 443: proto = "https" hip = "[{}]".format(ip) if ":" in ip else ip msgs.append(t.format(proto, hip, port, desc)) is_ext = "external" in unicode(desc) qrt = qr1 if is_ext else qr2 try: qrt[ip].append(port) except: qrt[ip] = [port] if not self.args.wintitle: continue if port in [80, 443]: ep = ip else: ep = "{}:{}".format(ip, port) hits = [] if "pub" in title_vars and is_ext: hits.append(("pub", ep)) if "pub" in title_vars or "all" in title_vars: hits.append(("all", ep)) for var in title_vars: if var.startswith("ip-") and ep.startswith(var[3:]): hits.append((var, ep)) for tk, tv in hits: try: title_tab[tk][tv] = 1 except: title_tab[tk] = {tv: 1} if msgs: for t in msgs: self.log("tcpsrv", t) if self.args.wintitle: self._set_wintitle(title_tab) else: print("\n", end="") for fn, ls in ( (self.args.wr_h_eps, listening_on), (self.args.wr_h_aon, accessible_on), ): if fn: with open(fn, "wb") as f: f.write(("\n".join(ls)).encode("utf-8")) if self.args.qr or self.args.qrs: self.qr = self._qr(qr1, qr2) def nlog(self, msg: str, c: Union[int, str] = 0) -> None: self.log("tcpsrv", msg, c) def _listen(self, ip: str, port: int) -> None: uds_perm = uds_gid = -1 bound: Optional[socket.socket] = None tcp = False if "unix:" in ip: ipv = socket.AF_UNIX uds = ip.split(":") ip = uds[-1] if len(uds) > 2: uds_perm = int(uds[1], 8) if len(uds) > 3: try: uds_gid = int(uds[2]) except: import grp uds_gid = grp.getgrnam(uds[2]).gr_gid elif "fd:" in ip: fd = ip[3:] bound = socket.socket(fileno=int(fd)) tcp = bound.proto == socket.IPPROTO_TCP ipv = bound.family elif ":" in ip: tcp = True ipv = socket.AF_INET6 else: tcp = True ipv = socket.AF_INET srv = bound or socket.socket(ipv, socket.SOCK_STREAM) if not ANYWIN or self.args.reuseaddr: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if tcp: srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) srv.settimeout(None) # < does not inherit, ^ opts above do try: srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) except: pass # will create another ipv4 socket instead if getattr(self.args, "freebind", False): srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1) if bound: self.srv.append(srv) return try: if tcp: if self.args.http_no_tcp: self.seen_eps.append((ip, port)) return srv.bind((ip, port)) else: if ANYWIN or self.args.rm_sck: if os.path.exists(ip): os.unlink(ip) srv.bind(ip) if uds_gid != -1: os.chown(ip, -1, uds_gid) if uds_perm != -1: os.chmod(ip, uds_perm) else: tf = "%s.%d" % (ip, os.getpid()) if os.path.exists(tf): os.unlink(tf) srv.bind(tf) if uds_gid != -1: os.chown(tf, -1, uds_gid) if uds_perm != -1: os.chmod(tf, uds_perm) atomic_move(self.nlog, tf, ip, VF_CAREFUL) sport = srv.getsockname()[1] if tcp else port if port != sport: # linux 6.0.16 lets you bind a port which is in use # except it just gives you a random port instead raise OSError(E_ADDR_IN_USE[0], "") self.srv.append(srv) except (OSError, socket.error) as ex: try: srv.close() except: pass e = "" if ex.errno in E_ADDR_IN_USE: e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip) if not tcp: e = "\033[1;31munix-socket {} is busy\033[0m".format(ip) elif ex.errno in E_ADDR_NOT_AVAIL: e = "\033[1;31minterface {} does not exist\033[0m".format(ip) if not e: if not tcp: t = "\n\n\n NOTE: this crash may be due to a unix-socket bug; try --rm-sck\n" self.log("tcpsrv", t, 2) raise if not tcp and not self.args.rm_sck: e += "; maybe this is a bug? try --rm-sck" raise Exception(e) def run(self) -> None: all_eps = [x.getsockname()[:2] for x in self.srv] bound: list[tuple[str, int]] = [] srvs: list[socket.socket] = [] for srv in self.srv: if srv.family == socket.AF_UNIX: tcp = False ip = re.sub(r"\.[0-9]+$", "", srv.getsockname()) port = 0 else: tcp = True ip, port = srv.getsockname()[:2] if ip == IP6ALL: ip = "::" # jython try: srv.listen(self.args.nc) try: ok = srv.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN) except: ok = 1 # macos if not ok: # some linux don't throw on listen(0.0.0.0) after listen(::) raise Exception("failed to listen on {}".format(srv.getsockname())) except: if ip == "0.0.0.0" and ("::", port) in bound: # dualstack srv.close() continue if ip == "::" and ("0.0.0.0", port) in all_eps: # no ipv6 srv.close() continue t = "\n\nERROR: could not open listening socket, probably because one of the server ports ({}) is busy on one of the requested interfaces ({}); avoid this issue by specifying a different port (-p 3939) and/or a specific interface to listen on (-i 192.168.56.1)\n" self.log("tcpsrv", t.format(port, ip), 1) raise bound.append((ip, port)) srvs.append(srv) fno = srv.fileno() if tcp: hip = "[{}]".format(ip) if ":" in ip else ip msg = "listening @ {}:{} f{} p{}".format(hip, port, fno, os.getpid()) else: msg = "listening @ {} f{} p{}".format(ip, fno, os.getpid()) self.log("tcpsrv", msg) if self.args.q: print(msg) self.hub.broker.say("httpsrv.listen", srv) self.srv = srvs self.bound = bound self.seen_eps = list(set(self.seen_eps + bound)) self.nsrv = len(srvs) self._distribute_netdevs() def _distribute_netdevs(self): self.hub.broker.say("httpsrv.set_netdevs", self.netdevs) self.hub.start_zeroconf() gencert(self.log, self.args, self.netdevs) self.hub.restart_sftpd() self.hub.restart_ftpd() self.hub.restart_tftpd() def shutdown(self) -> None: self.stopping = True try: for srv in self.srv: srv.close() except: pass self.log("tcpsrv", "ok bye") def netmon(self): while not self.stopping: time.sleep(self.args.z_chk) netdevs = self.detect_interfaces(self.args.i) if not netdevs: continue add = [] rem = [] for k, v in netdevs.items(): if k not in self.netdevs: add.append("\n\033[32m added %s = %s" % (k, v)) for k, v in self.netdevs.items(): if k not in netdevs: rem.append("\n\033[33mremoved %s = %s" % (k, v)) t = "network change detected:%s%s" self.log("tcpsrv", t % ("".join(add), "".join(rem)), 3) self.netdevs = netdevs self._distribute_netdevs() def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))] nics = get_adapters(True) eps: dict[str, Netdev] = {} for nic in nics: for nip in nic.ips: ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip sip = "{}/{}".format(ipa, nip.network_prefix) nd = Netdev(sip, nic.index or 0, nic.nice_name, "") eps[sip] = nd try: idx = socket.if_nametoindex(nd.name) if idx and idx != nd.idx: t = "netdev idx mismatch; ifaddr={} cpython={}" self.log("tcpsrv", t.format(nd.idx, idx), 3) nd.idx = idx except: pass netlist = str(sorted(eps.items())) if netlist == self.netlist and self.netdevs: return {} self.netlist = netlist if "0.0.0.0" not in listen_ips and "::" not in listen_ips: eps = {k: v for k, v in eps.items() if k.split("/")[0] in listen_ips} try: ext_devs = list(self._extdevs_nix()) ext_ips = [k for k, v in eps.items() if v.name in ext_devs] ext_ips = [x.split("/")[0] for x in ext_ips] if not ext_ips: raise Exception() except: rt = self._defroute() ext_ips = [rt] if rt else [] for lip in listen_ips: if not ext_ips or lip not in ["0.0.0.0", "::"] + ext_ips: continue desc = "\033[32mexternal" ips = ext_ips if lip in ["0.0.0.0", "::"] else [lip] for ip in ips: ip = next((x for x in eps if x.startswith(ip + "/")), "") if ip and "external" not in eps[ip].desc: eps[ip].desc += ", " + desc return eps def _extdevs_nix(self) -> Generator[str, None, None]: with open("/proc/net/route", "rb") as f: next(f) for ln in f: r = ln.decode("utf-8").strip().split() if r[1] == "0" * 8 and int(r[3], 16) & 2: yield r[0] def _defroute(self) -> str: ret = "" s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for ip in [ "10.254.39.23", "172.31.39.23", "192.168.39.23", "239.254.39.23", "169.254.39.23", # could add 1.1.1.1 as a final fallback # but external connections is kinshi ]: try: s.connect((ip, 1)) ret = s.getsockname()[0] break except (OSError, socket.error) as ex: if ex.errno in E_ACCESS: self.log("tcpsrv", "eaccess {} (trying next)".format(ip)) elif ex.errno not in E_UNREACH: self.log("tcpsrv", "route lookup failed; err {}".format(ex.errno)) s.close() return ret def _set_wintitle(self, vs: dict[str, dict[str, int]]) -> None: vs["all"] = vs.get("all", {"Local-Only": 1}) vs["pub"] = vs.get("pub", vs["all"]) vs2 = {} for k, eps in vs.items(): filt = {ep: 1 for ep in eps if ":" not in ep} have = set(filt) for ep in sorted(eps): ip = ep.split(":")[0] if ip not in have: have.add(ip) filt[ep] = 1 lo = [x for x in filt if x.startswith("127.")] if len(filt) > 3 and lo: for ip in lo: filt.pop(ip) vs2[k] = filt title = "" vs = vs2 for p in self.args.wintitle.split(" "): if p.startswith("$"): seps = list(sorted(vs.get(p[1:], {"(None)": 1}).keys())) p = ", ".join(seps[:3]) if len(seps) > 3: p += ", ..." title += "{} ".format(p) print("\033]0;{}\033\\\n".format(title), file=sys.stderr, end="") sys.stderr.flush() def _qr(self, t1: dict[str, list[int]], t2: dict[str, list[int]]) -> str: t2c = {zs: zli for zs, zli in t2.items() if zs in ("127.0.0.1", "::1")} t2b = {zs: zli for zs, zli in t2.items() if ":" in zs and zs not in t2c} t2 = {zs: zli for zs, zli in t2.items() if zs not in t2b and zs not in t2c} t2.update(t2b) # first ipv4, then ipv6... t2.update(t2c) # ...and finally localhost ip = None ips = list(t1) + list(t2) qri = self.args.qri if self.args.zm and not qri and ips: name = self.args.name + ".local" t1[name] = next(v for v in (t1 or t2).values()) ips = [name] + ips for ip in ips: if ip.startswith(qri) or qri == ".": break ip = "" if not ip: # maybe /bin/ip is missing or smth ip = qri if not ip: return "" hip = "[%s]" % (ip,) if ":" in ip else ip if self.args.http_only: https = "" elif self.args.https_only: https = "s" else: https = "s" if self.args.qrs else "" ports = t1.get(ip, t2.get(ip, [])) dport = 443 if https else 80 port = "" if dport in ports or not ports else ":{}".format(ports[0]) txt = "http{}://{}{}/{}".format(https, hip, port, self.args.qrl) btxt = txt.encode("utf-8") if PY2: btxt = sunpack(b"B" * len(btxt), btxt) fg = self.args.qr_fg bg = self.args.qr_bg nocolor = fg == -1 if nocolor: fg = 0 pad = self.args.qrp zoom = self.args.qrz qrc = qrgen(btxt) for zs in self.args.qr_file or []: self._qr2file(qrc, zs) if zoom == 0: try: tw, th = termsize() tsz = min(tw // 2, th) zoom = 1 if qrc.size + pad * 2 >= tsz else 2 except: zoom = 1 qr = qr2txt(qrc, zoom, pad) if self.args.no_ansi: return "{}\n{}".format(txt, qr) halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m" if not fg: halfc = "\033[0;40m{1}\033[0;47m" if nocolor: halfc = "\033[0;7m{1}\033[0m" def ansify(m: re.Match) -> str: return halfc.format(fg, " " * len(m.group(1)), bg) if zoom > 1: qr = re.sub("(█+)", ansify, qr) qr = qr.replace("\n", "\033[K\n") + "\033[K" # win10do cc = " \033[0;38;5;{0};47;48;5;{1}m" if fg else " \033[0;30;47m" if nocolor: cc = " \033[0m" t = cc + "\n{2}\033[999G\033[0m\033[J" t = t.format(fg, bg, qr) if ANYWIN: # prevent color loss on terminal resize t = t.replace("\n", "`\n`") return txt + t def _qr2file(self, qrc: QrCode, txt: str): if ".txt:" in txt or ".svg:" in txt: ap, zs1, zs2 = txt.rsplit(":", 2) bg = fg = "" else: ap, zs1, zs2, bg, fg = txt.rsplit(":", 4) zoom = int(zs1) pad = int(zs2) if ap.endswith(".txt"): if zoom not in (1, 2): raise Exception("invalid zoom for qr.txt; must be 1 or 2") with open(ap, "wb") as f: f.write(qr2txt(qrc, zoom, pad).encode("utf-8")) elif ap.endswith(".svg"): with open(ap, "wb") as f: f.write(qr2svg(qrc, pad).encode("utf-8")) else: qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap) def _h2i(self, hs): try: return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4)) except: return None ================================================ FILE: copyparty/tftpd.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals try: from types import SimpleNamespace except: class SimpleNamespace(object): def __init__(self, **attr): self.__dict__.update(attr) import logging import os import re import socket import stat import threading import time from datetime import datetime try: import inspect except: pass from partftpy import ( TftpContexts, TftpPacketFactory, TftpPacketTypes, TftpServer, TftpStates, ) from partftpy.TftpShared import TftpException from .__init__ import EXE, PY2, TYPE_CHECKING from .authsrv import VFS from .bos import bos from .util import ( UTC, BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, set_fperms, undot, vjoin, vsplit, ) if True: # pylint: disable=using-constant-test from typing import Any, Union if TYPE_CHECKING: from .svchub import SvcHub if PY2: range = xrange # type: ignore lg = logging.getLogger("tftp") debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error) def noop(*a, **ka) -> None: pass def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool: info("connection from %s:%s", raddress, rport) ret = _sinitial[0](self, pkt, raddress, rport) nm = _hub[0].args.tftp_ipa_nm if nm and not nm.map(raddress): yeet("client rejected (--tftp-ipa): %s" % (raddress,)) return ret # patch ipa-check into partftpd (part 1/2) _hub: list["SvcHub"] = [] _sinitial: list[Any] = [] class Tftpd(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.args = hub.args self.asrv = hub.asrv self.log = hub.log self.mutex = threading.Lock() _hub[:] = [] _hub.append(hub) lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) for x in ["partftpy", "partftpy.TftpStates", "partftpy.TftpServer"]: lgr = logging.getLogger(x) lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) if not self.args.tftpv and not self.args.tftpvv: # contexts -> states -> packettypes -> shared # contexts -> packetfactory # packetfactory -> packettypes Cs = [ TftpPacketTypes, TftpPacketFactory, TftpStates, TftpContexts, TftpServer, ] cbak = [] if not self.args.tftp_no_fast and not EXE and not PY2: try: ptn = re.compile(r"(^\s*)log\.debug\(.*\)$") for C in Cs: cbak.append(C.__dict__) src1 = inspect.getsource(C).split("\n") src2 = "\n".join([ptn.sub("\\1pass", ln) for ln in src1]) cfn = C.__spec__.origin exec (compile(src2, filename=cfn, mode="exec"), C.__dict__) except Exception: t = "failed to optimize tftp code; run with --tftp-no-fast if there are issues:\n" self.log("tftp", t + min_ex(), 3) for n, zd in enumerate(cbak): Cs[n].__dict__ = zd for C in Cs: C.log.debug = noop # patch ipa-check into partftpd (part 2/2) _sinitial[:] = [] _sinitial.append(TftpStates.TftpServerState.serverInitial) TftpStates.TftpServerState.serverInitial = _serverInitial # patch vfs into partftpy TftpContexts.open = self._open TftpStates.open = self._open fos = SimpleNamespace() for k in os.__dict__: try: setattr(fos, k, getattr(os, k)) except: pass fos.access = self._access fos.mkdir = self._mkdir fos.unlink = self._unlink fos.sep = "/" TftpContexts.os = fos TftpServer.os = fos TftpStates.os = fos fop = SimpleNamespace() for k in os.path.__dict__: try: setattr(fop, k, getattr(os.path, k)) except: pass fop.abspath = self._p_abspath fop.exists = self._p_exists fop.isdir = self._p_isdir fop.normpath = self._p_normpath fos.path = fop self._disarm(fos) self.port = int(self.args.tftp) self.srv = [] self.ips = [] ports = [] if self.args.tftp_pr: p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")] ports = list(range(p1, p2 + 1)) ips = self.args.tftp_i if "::" in ips: ips.append("0.0.0.0") ips = [x for x in ips if not x.startswith(("unix:", "fd:"))] if self.args.tftp4: ips = [x for x in ips if ":" not in x] if not ips: t = "cannot start tftp-server; no compatible IPs in -i" self.nlog(t, 1) return ips = list(ODict.fromkeys(ips)) # dedup for ip in ips: name = "tftp_%s" % (ip,) Daemon(self._start, name, [ip, ports]) time.sleep(0.2) # give dualstack a chance def nlog(self, msg: str, c: Union[int, str] = 0) -> None: self.log("tftp", msg, c) def _start(self, ip, ports): fam = socket.AF_INET6 if ":" in ip else socket.AF_INET have_been_alive = False while True: srv = TftpServer.TftpServer("/", self._ls) with self.mutex: self.srv.append(srv) self.ips.append(ip) try: # this is the listen loop; it should block forever srv.listen(ip, self.port, af_family=fam, ports=ports) except: with self.mutex: self.srv.remove(srv) self.ips.remove(ip) try: srv.sock.close() except: pass try: bound = bool(srv.listenport) except: bound = False if bound: # this instance has managed to bind at least once have_been_alive = True if have_been_alive: t = "tftp server [%s]:%d crashed; restarting in 3 sec:\n%s" error(t, ip, self.port, min_ex()) time.sleep(3) continue # server failed to start; could be due to dualstack (ipv6 managed to bind and this is ipv4) if ip != "0.0.0.0" or "::" not in self.ips: # nope, it's fatal t = "tftp server [%s]:%d failed to start:\n%s" error(t, ip, self.port, min_ex()) # yep; ignore # (TODO: move the "listening @ ..." infolog in partftpy to # after the bind attempt so it doesn't print twice) return info("tftp server [%s]:%d terminated", ip, self.port) break def stop(self): with self.mutex: srvs = self.srv[:] for srv in srvs: srv.stop() def _v2a( self, caller: str, vpath: str, perms: list, *a: Any ) -> tuple[VFS, str, str]: vpath = vpath.replace("\\", "/").lstrip("/") if not perms: perms = [True, True] debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms) vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags: zs, fn = vsplit(vpath) if fn.lower() in vfs.flags["emb_all"]: vpath = vjoin(zs, "_wo_" + fn) vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) if not vfs.realpath: raise Exception("unmapped vfs") return vfs, vpath, vfs.canonical(rem) def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any: # generate file listing if vpath is dir.txt and return as file object if not force: vpath, fn = os.path.split(vpath.replace("\\", "/")) ptn = self.args.tftp_lsf if not ptn or not ptn.match(fn.lower()): return None tsdt = datetime.fromtimestamp vn, rem = self.asrv.vfs.get(vpath, "*", True, False) fsroot, vfs_ls, vfs_virt = vn.ls( rem, "*", not self.args.no_scandir, [[True, False]], throw=True, ) dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]) dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames] fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames] real1 = dirs1 + fils1 realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1] reals = [ ( "%04d-%02d-%02d %02d:%02d:%02d" % ( zd.year, zd.month, zd.day, zd.hour, zd.minute, zd.second, ), sz, fn, ) for zd, sz, fn in realt ] virs = [("????-??-?? ??:??:??", 0, k + "/") for k in vfs_virt.keys()] ls = virs + reals if "*" not in vn.axs.udot: names = set(exclude_dotfiles([x[2] for x in ls])) ls = [x for x in ls if x[2] in names] try: biggest = max([x[1] for x in ls]) except: biggest = 0 perms = [] if "*" in vn.axs.uread: perms.append("read") if "*" in vn.axs.udot: perms.append("hidden") if "*" in vn.axs.uwrite: if "*" in vn.axs.udel: perms.append("overwrite") else: perms.append("write") fmt = "{{}} {{:{},}} {{}}" fmt = fmt.format(len("{:,}".format(biggest))) retl = ["# permissions: %s" % (", ".join(perms),)] retl += [fmt.format(*x) for x in ls] ret = "\n".join(retl).encode("utf-8", "replace") return BytesIO(ret + b"\n") def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any: rd = wr = False if mode == "rb": rd = True elif mode == "wb": wr = True else: raise Exception("bad mode %s" % (mode,)) vfs, vpath, ap = self._v2a("open", vpath, [rd, wr]) if wr: if "*" not in vfs.axs.uwrite: yeet("blocked write; folder not world-writable: /%s" % (vpath,)) if bos.path.exists(ap) and "*" not in vfs.axs.udel: yeet("blocked write; folder not world-deletable: /%s" % (vpath,)) xbu = vfs.flags.get("xbu") if xbu: hr = runhook( self.nlog, None, self.hub.up2k, "xbu.tftpd", xbu, ap, vpath, "", "", "", 0, 0, "8.3.8.7", time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vpath,) yeet(t) if not self.args.tftp_nols and bos.path.isdir(ap): return self._ls(vpath, "", 0, True) if not a: a = (self.args.iobuf,) ret = open(ap, mode, *a, **ka) if wr and "fperms" in vfs.flags: set_fperms(ret, vfs.flags) return ret def _mkdir(self, vpath: str, *a) -> None: vfs, _, ap = self._v2a("mkdir", vpath, [False, True]) if "*" not in vfs.axs.uwrite: yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,)) bos.mkdir(ap, vfs.flags["chmod_d"]) if "chown" in vfs.flags: bos.chown(ap, vfs.flags["uid"], vfs.flags["gid"]) def _unlink(self, vpath: str) -> None: # return bos.unlink(self._v2a("stat", vpath, *a)[1]) vfs, _, ap = self._v2a("delete", vpath, [True, False, False, True]) try: inf = bos.stat(ap) except: return if not stat.S_ISREG(inf.st_mode) or inf.st_size: yeet("attempted delete of non-empty file") vpath = vpath.replace("\\", "/").lstrip("/") self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False, False) def _access(self, *a: Any) -> bool: return True def _p_abspath(self, vpath: str) -> str: return "/" + undot(vpath) def _p_normpath(self, *a: Any) -> str: return "" def _p_exists(self, vpath: str) -> bool: try: ap = self._v2a("p.exists", vpath, [False, False])[2] bos.stat(ap) return True except: return vpath == "/" def _p_isdir(self, vpath: str) -> bool: try: st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[2]) ret = stat.S_ISDIR(st.st_mode) return ret except: return vpath == "/" def _hook(self, *a: Any, **ka: Any) -> None: src = inspect.currentframe().f_back.f_code.co_name error("\033[31m%s:hook(%s)\033[0m", src, a) raise Exception("nope") def _disarm(self, fos: SimpleNamespace) -> None: fos.chmod = self._hook fos.chown = self._hook fos.close = self._hook fos.ftruncate = self._hook fos.lchown = self._hook fos.link = self._hook fos.listdir = self._hook fos.lstat = self._hook fos.open = self._hook fos.remove = self._hook fos.rename = self._hook fos.replace = self._hook fos.scandir = self._hook fos.stat = self._hook fos.symlink = self._hook fos.truncate = self._hook fos.utime = self._hook fos.walk = self._hook fos.path.expanduser = self._hook fos.path.expandvars = self._hook fos.path.getatime = self._hook fos.path.getctime = self._hook fos.path.getmtime = self._hook fos.path.getsize = self._hook fos.path.isabs = self._hook fos.path.isfile = self._hook fos.path.islink = self._hook fos.path.realpath = self._hook def yeet(msg: str) -> None: warning(msg) raise TftpException(msg) ================================================ FILE: copyparty/th_cli.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import errno import os import stat from .__init__ import TYPE_CHECKING from .authsrv import VFS from .bos import bos from .th_srv import EXTS_AC, H_PIL_JXL, H_PIL_WEBP, thumb_path from .util import Cooldown, Pebkac if True: # pylint: disable=using-constant-test from typing import Optional, Union if TYPE_CHECKING: from .httpsrv import HttpSrv IOERROR = "reading the file was denied by the server os; either due to filesystem permissions, selinux, apparmor, or similar:\n%r" IMG_EXTS = set(["webp", "jpg", "png", "jxl"]) class ThumbCli(object): def __init__(self, hsrv: "HttpSrv") -> None: self.broker = hsrv.broker self.log_func = hsrv.log self.args = hsrv.args self.asrv = hsrv.asrv # cache on both sides for less broker spam self.cooldown = Cooldown(self.args.th_poke) try: c = hsrv.th_cfg if not c: raise Exception() except: c = { k: set() for k in ["thumbable", "pil", "vips", "raw", "ffi", "ffv", "ffa"] } self.thumbable = c["thumbable"] self.fmt_pil = c["pil"] self.fmt_vips = c["vips"] self.fmt_raw = c["raw"] self.fmt_ffi = c["ffi"] self.fmt_ffv = c["ffv"] self.fmt_ffa = c["ffa"] # defer args.th_ff_jpg, can change at runtime nonpil = next((x for x in self.args.th_dec if x in ("vips", "ff")), None) self.can_webp = H_PIL_WEBP or nonpil self.can_jxl = H_PIL_JXL or nonpil def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func("thumbcli", msg, c) def get(self, dbv: VFS, rem: str, mtime: float, fmt: str) -> Optional[str]: ptop = dbv.realpath ext = rem.rsplit(".")[-1].lower() if ext not in self.thumbable or "dthumb" in dbv.flags: return None is_vid = ext in self.fmt_ffv if is_vid and "dvthumb" in dbv.flags: return None want_opus = fmt in EXTS_AC is_au = ext in self.fmt_ffa is_vau = want_opus and ext in self.fmt_ffv if is_au or is_vau: if want_opus: if self.args.no_acode: return None elif fmt == "caf" and self.args.no_caf: fmt = "mp3" elif fmt == "owa" and self.args.no_owa: fmt = "mp3" else: if "dathumb" in dbv.flags: return None elif want_opus: return None is_img = not is_vid and not is_au if is_img and "dithumb" in dbv.flags: return None preferred = self.args.th_dec[0] if self.args.th_dec else "" if rem.startswith(".hist/th/") and rem.split(".")[-1] in IMG_EXTS: return os.path.join(ptop, rem) if fmt[:1] in "jwx" and fmt != "wav": sfmt = fmt[:1] if sfmt == "j" and self.args.th_no_jpg: sfmt = "w" if sfmt == "w": if ( self.args.th_no_webp or not self.can_webp or (self.args.th_ff_jpg and (not is_img or preferred == "ff")) ): sfmt = "j" if sfmt == "x": if self.args.th_no_jxl or not self.can_jxl: sfmt = "w" vf_crop = dbv.flags["crop"] vf_th3x = dbv.flags["th3x"] if "f" in vf_crop: sfmt += "f" if "n" in vf_crop else "" else: sfmt += "f" if "f" in fmt else "" if "f" in vf_th3x: sfmt += "3" if "y" in vf_th3x else "" else: sfmt += "3" if "3" in fmt else "" fmt = sfmt elif fmt[:1] == "p" and not is_au and not is_vid: t = "cannot thumbnail %r: png only allowed for waveforms" self.log(t % (rem,), 6) return None histpath = self.asrv.vfs.histtab.get(ptop) if not histpath: self.log("no histpath for %r" % (ptop,)) return None tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa) tpaths = [tpath] fmtc = fmt[:1] if fmtc == "w" and fmt != "wav": # also check for jpg (maybe webp is unavailable) tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg") elif fmtc == "x": tpaths.append(tpath.rsplit(".", 1)[0] + ".webp") ret = None abort = False for tp in tpaths: try: st = bos.stat(tp) if st.st_size: ret = tpath = tp fmt = ret.rsplit(".")[1] break else: abort = True except: pass if ret: tdir = os.path.dirname(tpath) if self.cooldown.poke(tdir): self.broker.say("thumbsrv.poke", tdir) if want_opus: # audio files expire individually if self.cooldown.poke(tpath): self.broker.say("thumbsrv.poke", tpath) return ret if abort: return None ap = os.path.join(ptop, rem) try: st = bos.stat(ap) if not st.st_size or not stat.S_ISREG(st.st_mode): return None with open(ap, "rb", 4) as f: if not f.read(4): raise Exception() except OSError as ex: if ex.errno == errno.ENOENT: raise Pebkac(404) else: raise Pebkac(500, IOERROR % (ex,)) except Exception as ex: raise Pebkac(500, IOERROR % (ex,)) x = self.broker.ask("thumbsrv.get", ptop, rem, mtime, fmt) return x.get() # type: ignore ================================================ FILE: copyparty/th_srv.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import hashlib import io import logging import os import re import shlex import shutil import subprocess as sp import tempfile import threading import time from queue import Queue from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .authsrv import VFS from .bos import bos from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe from .util import BytesIO # type: ignore from .util import ( FFMPEG_URL, VF_CAREFUL, Cooldown, Daemon, afsenc, atomic_move, fsenc, min_ex, runcmd, statdir, ub64enc, vsplit, wunlink, ) if True: # pylint: disable=using-constant-test from typing import Any, Callable, Optional, Union if TYPE_CHECKING: from .svchub import SvcHub if PY2: range = xrange # type: ignore HAVE_PIL = False HAVE_PILF = False H_PIL_HEIF = False H_PIL_AVIF = False H_PIL_WEBP = False H_PIL_JXL = False TH_CH = {"j": "jpg", "p": "png", "w": "webp", "x": "jxl"} EXTS_TH = set(["jpg", "webp", "jxl", "png"]) EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"]) EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split()) PTN_TS = re.compile("^-?[0-9a-f]{8,10}$") # for n in {1..100}; do rm -rf /home/ed/Pictures/wp/.hist/th/ ; python3 -m copyparty -qv /home/ed/Pictures/wp/::r --th-no-webp --th-qv $n --th-dec pil >/dev/null 2>&1 & p=$!; printf '\033[A\033[J%3d ' $n; while true; do sleep 0.1; curl -s 127.1:3923 >/dev/null && break; done; curl -s '127.1:3923/?tar=j' >/dev/null ; cat /home/ed/Pictures/wp/.hist/th/1n/bs/1nBsjDetfie1iDq3y2D4YzF5/*.* | wc -c; kill $p; wait >/dev/null 2>&1; done # filesize-equivalent, not quality (ff looks much shittier) FF_JPG_Q = { 0: b"30", # 0 1: b"30", # 5 2: b"30", # 10 3: b"30", # 15 4: b"28", # 20 5: b"21", # 25 6: b"17", # 30 7: b"15", # 35 8: b"13", # 40 9: b"12", # 45 10: b"11", # 50 11: b"10", # 55 12: b"9", # 60 13: b"8", # 65 14: b"7", # 70 15: b"6", # 75 16: b"5", # 80 17: b"4", # 85 18: b"3", # 90 19: b"2", # 95 20: b"2", # 100 } # FF_JPG_Q = {xn: ("%d" % (xn,)).encode("ascii") for xn in range(2, 33)} VIPS_JPG_Q = { 0: 4, # 0 1: 7, # 5 2: 12, # 10 3: 17, # 15 4: 22, # 20 5: 27, # 25 6: 32, # 30 7: 37, # 35 8: 42, # 40 9: 47, # 45 10: 52, # 50 11: 56, # 55 12: 61, # 60 13: 66, # 65 14: 71, # 70 15: 75, # 75 16: 80, # 80 17: 85, # 85 18: 89, # 90 (vips explodes past this point) 19: 91, # 95 20: 97, # 100 } try: if os.environ.get("PRTY_NO_PIL"): raise Exception() from PIL import ExifTags, Image, ImageFont, ImageOps HAVE_PIL = True try: if os.environ.get("PRTY_NO_PILF"): raise Exception() ImageFont.load_default(size=16) HAVE_PILF = True except: pass try: if os.environ.get("PRTY_NO_PIL_WEBP"): raise Exception() Image.new("RGB", (2, 2)).save(BytesIO(), format="webp") H_PIL_WEBP = True except: pass try: if os.environ.get("PRTY_NO_PIL_JXL"): raise Exception() try: import pillow_jxl except ImportError: pass Image.new("RGB", (2, 2)).save(BytesIO(), format="jxl") H_PIL_JXL = True except: pass try: if os.environ.get("PRTY_NO_PIL_HEIF"): raise Exception() try: from pillow_heif import register_heif_opener except ImportError: from pyheif_pillow_opener import register_heif_opener register_heif_opener() H_PIL_HEIF = True except: pass try: if os.environ.get("PRTY_NO_PIL_AVIF"): raise Exception() if ".avif" in Image.registered_extensions(): H_PIL_AVIF = True raise Exception() import pillow_avif # noqa: F401 # pylint: disable=unused-import H_PIL_AVIF = True except: pass logging.getLogger("PIL").setLevel(logging.WARNING) except: pass try: if os.environ.get("PRTY_NO_VIPS"): raise ImportError() HAVE_VIPS = True import pyvips pyvips.cache_set_max(0) logging.getLogger("pyvips").setLevel(logging.WARNING) except Exception as e: HAVE_VIPS = False if not isinstance(e, ImportError): logging.warning("libvips found, but failed to load: " + str(e)) try: if os.environ.get("PRTY_NO_RAW"): raise Exception() HAVE_RAW = True import rawpy logging.getLogger("rawpy").setLevel(logging.WARNING) except: HAVE_RAW = False th_dir_cache = {} def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -> str: # base16 = 16 = 256 # b64-lc = 38 = 1444 # base64 = 64 = 4096 rd, fn = vsplit(rem) if not rd: rd = "\ntop" # spectrograms are never cropped; strip fullsize flag ext = rem.split(".")[-1].lower() if ext in ffa and fmt[:2] in ("wf", "jf", "xf"): fmt = fmt.replace("f", "") dcache = th_dir_cache rd_key = rd + "\n" + fmt rd = dcache.get(rd_key) if not rd: h = hashlib.sha512(afsenc(rd_key)).digest() b64 = ub64enc(h).decode("ascii")[:24] rd = ("%s/%s/" % (b64[:2], b64[2:4])).lower() + b64 if len(dcache) > 9001: dcache.clear() dcache[rd_key] = rd # could keep original filenames but this is safer re pathlen h = hashlib.sha512(afsenc(fn)).digest() fn = ub64enc(h).decode("ascii")[:24] if fmt in EXTS_AC: cat = "ac" else: fmt = TH_CH[fmt[:1]] cat = "th" return "%s/%s/%s/%s.%x.%s" % (histpath, cat, rd, fn, int(mtime), fmt) class ThumbSrv(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.asrv = hub.asrv self.args = hub.args self.log_func = hub.log self.poke_cd = Cooldown(self.args.th_poke) self.mutex = threading.Lock() self.busy: dict[str, list[threading.Condition]] = {} self.untemp: dict[str, list[str]] = {} self.ram: dict[str, float] = {} self.memcond = threading.Condition(self.mutex) self.stopping = False self.rm_nullthumbs = True # forget failed conversions on startup self.nthr = max(1, self.args.th_mt) self.exts_spec_unsafe = set(self.args.th_spec_cnv.split(",")) self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4) for n in range(self.nthr): Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr)) want_ff = not self.args.no_vthumb or not self.args.no_athumb if want_ff and (not HAVE_FFMPEG or not HAVE_FFPROBE): missing = [] if not HAVE_FFMPEG: missing.append("FFmpeg") if not HAVE_FFPROBE: missing.append("FFprobe") msg = "cannot create audio/video thumbnails because some of the required programs are not available: " msg += ", ".join(missing) self.log(msg, c=3) if ANYWIN and self.args.no_acode: self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3) if self.args.th_clean: Daemon(self.cleaner, "thumb.cln") ( self.fmt_pil, self.fmt_vips, self.fmt_raw, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa, ) = [ set(y.split(",")) for y in [ self.args.th_r_pil, self.args.th_r_vips, self.args.th_r_raw, self.args.th_r_ffi, self.args.th_r_ffv, self.args.th_r_ffa, ] ] if not H_PIL_HEIF: for f in "heif heifs heic heics".split(" "): self.fmt_pil.discard(f) if not H_PIL_AVIF: for f in "avif avifs".split(" "): self.fmt_pil.discard(f) if not H_PIL_WEBP: for f in "webp".split(" "): self.fmt_pil.discard(f) if not H_PIL_JXL: for f in "jxl".split(" "): self.fmt_pil.discard(f) self.thumbable: set[str] = set() if "pil" in self.args.th_dec: self.thumbable |= self.fmt_pil if "vips" in self.args.th_dec: self.thumbable |= self.fmt_vips if "raw" in self.args.th_dec: self.thumbable |= self.fmt_raw if "ff" in self.args.th_dec: for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: self.thumbable |= zss def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func("thumb", msg, c) def shutdown(self) -> None: self.stopping = True Daemon(self._fire_sentinels, "thumbstopper") def _fire_sentinels(self): for _ in range(self.nthr): self.q.put(None) def stopped(self) -> bool: with self.mutex: return not self.nthr def getres(self, vn: VFS, fmt: str) -> tuple[int, int]: mul = 3 if "3" in fmt else 1 w, h = vn.flags["thsize"].split("x") return int(w) * mul, int(h) * mul def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]: histpath = self.asrv.vfs.histtab.get(ptop) if not histpath: self.log("no histpath for %r" % (ptop,)) return None tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa) abspath = os.path.join(ptop, rem) cond = threading.Condition(self.mutex) do_conv = False with self.mutex: try: self.busy[tpath].append(cond) self.log("joined waiting room for %r" % (tpath,)) except: thdir = os.path.dirname(tpath) chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755 bos.makedirs(os.path.join(thdir, "w"), vf=chmod) inf_path = os.path.join(thdir, "dir.txt") if not bos.path.exists(inf_path): with open(inf_path, "wb") as f: f.write(afsenc(os.path.dirname(abspath))) self.writevolcfg(histpath) self.busy[tpath] = [cond] do_conv = True if do_conv: allvols = list(self.asrv.vfs.all_vols.values()) vn = next((x for x in allvols if x.realpath == ptop), None) if not vn: self.log("ptop %r not in %s" % (ptop, allvols), 3) vn = self.asrv.vfs.all_aps[0][1][0] self.q.put((abspath, tpath, fmt, vn)) self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6) while not self.stopping: with self.mutex: if tpath not in self.busy: break with cond: cond.wait(3) try: st = bos.stat(tpath) if st.st_size: self.poke(tpath) return tpath except: pass return None def getcfg(self) -> dict[str, set[str]]: return { "thumbable": self.thumbable, "pil": self.fmt_pil, "vips": self.fmt_vips, "raw": self.fmt_raw, "ffi": self.fmt_ffi, "ffv": self.fmt_ffv, "ffa": self.fmt_ffa, } def volcfgi(self, vn: VFS) -> str: ret = [] zs = "th_dec th_no_webp th_no_jpg" for zs in zs.split(" "): ret.append("%s(%s)\n" % (zs, getattr(self.args, zs))) zs = "th_qv th_qvx thsize th_spec_p convt" for zs in zs.split(" "): ret.append("%s(%s)\n" % (zs, vn.flags.get(zs))) return "".join(ret) def volcfga(self, vn: VFS) -> str: ret = [] zs = "q_opus q_mp3" for zs in zs.split(" "): ret.append("%s(%s)\n" % (zs, getattr(self.args, zs))) zs = "aconvt" for zs in zs.split(" "): ret.append("%s(%s)\n" % (zs, vn.flags.get(zs))) return "".join(ret) def writevolcfg(self, histpath: str) -> None: try: bos.stat(os.path.join(histpath, "th", "cfg.txt")) bos.stat(os.path.join(histpath, "ac", "cfg.txt")) return except: pass cfgi = cfga = "" for vn in self.asrv.vfs.all_vols.values(): if vn.histpath == histpath: cfgi = self.volcfgi(vn) cfga = self.volcfga(vn) break t = "writing thumbnailer-config %d,%d to %s" self.log(t % (len(cfgi), len(cfga), histpath)) chmod = bos.MKD_700 if self.args.free_umask else bos.MKD_755 for cfg, cat in ((cfgi, "th"), (cfga, "ac")): bos.makedirs(os.path.join(histpath, cat), vf=chmod) with open(os.path.join(histpath, cat, "cfg.txt"), "wb") as f: f.write(cfg.encode("utf-8")) def wait4ram(self, need: float, ttpath: str) -> None: ram = self.args.th_ram_max if need > ram * 0.99: t = "file too big; need %.2f GiB RAM, but --th-ram-max is only %.1f" raise Exception(t % (need, ram)) while True: with self.mutex: used = sum([v for k, v in self.ram.items() if k != ttpath]) + need if used < ram: # self.log("XXX self.ram: %s" % (self.ram,), 5) self.ram[ttpath] = need return with self.memcond: # self.log("at RAM limit; used %.2f GiB, need %.2f more" % (used-need, need), 1) self.memcond.wait(3) def worker(self) -> None: while not self.stopping: task = self.q.get() if not task: break abspath, tpath, fmt, vn = task ext = abspath.split(".")[-1].lower() png_ok = False funs = [] if ext in self.args.au_unpk: ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn) else: ap_unpk = abspath if ap_unpk and not bos.path.exists(tpath): tex = tpath.rsplit(".", 1)[-1] want_mp3 = tex == "mp3" want_opus = tex in ("opus", "owa", "caf") want_flac = tex == "flac" want_wav = tex == "wav" want_png = tex == "png" want_au = want_mp3 or want_opus or want_flac or want_wav for lib in self.args.th_dec: can_au = lib == "ff" and ( ext in self.fmt_ffa or ext in self.fmt_ffv ) if lib == "pil" and ext in self.fmt_pil and tex in self.fmt_pil: funs.append(self.conv_pil) elif lib == "vips" and ext in self.fmt_vips: funs.append(self.conv_vips) elif lib == "raw" and ext in self.fmt_raw: funs.append(self.conv_raw) elif can_au and (want_png or want_au): if want_opus: funs.append(self.conv_opus) elif want_mp3: funs.append(self.conv_mp3) elif want_flac: funs.append(self.conv_flac) elif want_wav: funs.append(self.conv_wav) elif want_png: funs.append(self.conv_waves) png_ok = True elif lib == "ff" and (ext in self.fmt_ffi or ext in self.fmt_ffv): funs.append(self.conv_ffmpeg) elif lib == "ff" and ext in self.fmt_ffa and not want_au: funs.append(self.conv_spec) tdir, tfn = os.path.split(tpath) ttpath = os.path.join(tdir, "w", tfn) try: wunlink(self.log, ttpath, vn.flags) except: pass conv_ok = False for fun in funs: try: if not png_ok and tpath.endswith(".png"): raise Exception("png only allowed for waveforms") fun(ap_unpk, ttpath, fmt, vn) conv_ok = True break except Exception as ex: r321 = getattr(ex, "returncode", 0) == 321 msg = "%s could not create thumbnail of %r\n%s" msg = msg % (fun.__name__, abspath, ex if r321 else min_ex()) c: Union[str, int] = 1 if " "Image.Image": # exif_transpose is expensive (loads full image + unconditional copy) res = self.getres(vn, fmt) r = max(*res) * 2 im.thumbnail((r, r), resample=Image.LANCZOS) try: k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation") exif = im.getexif() rot = int(exif[k]) del exif[k] except: rot = 1 rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270} if rot in rots: im = im.transpose(rots[rot]) if "f" in fmt: im.thumbnail(res, resample=Image.LANCZOS) else: iw, ih = im.size dw, dh = res res = (min(iw, dw), min(ih, dh)) im = ImageOps.fit(im, res, method=Image.LANCZOS) return im def conv_image_pil(self, im: "Image.Image", tpath: str, fmt: str, vn: VFS) -> None: try: im = self.fancy_pillow(im, fmt, vn) except Exception as ex: self.log("fancy_pillow {}".format(ex), "90") im.thumbnail(self.getres(vn, fmt)) fmts = ["RGB", "L"] zs = "th_qvx" if tpath.endswith(".jxl") else "th_qv" args = {"quality": vn.flags[zs]} if tpath.endswith(".webp"): # quality 80 = pillow-default # quality 75 = ffmpeg-default # method 0 = pillow-default, fast # method 4 = ffmpeg-default # method 6 = max, slow fmts.extend(("RGBA", "LA")) args["method"] = 6 else: # default q = 75 args["progressive"] = True if im.mode not in fmts: # print("conv {}".format(im.mode)) im = im.convert("RGB") im.save(tpath, **args) def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) with Image.open(fsenc(abspath)) as im: self.conv_image_pil(im, tpath, fmt, vn) def conv_image_vips( self, loader: "Callable[[int, dict], Any]", tpath: str, fmt: str, vn: VFS ) -> None: crops = ["centre", "none"] if "f" in fmt: crops = ["none"] w, h = self.getres(vn, fmt) kw = {"height": h, "size": "down", "intent": "relative"} img = None for c in crops: try: kw["crop"] = c img = loader(w, kw) break except: if c == crops[-1]: raise assert img # type: ignore # !rm args = {} qv = vn.flags["th_qv"] if tpath.endswith("jpg"): qv = VIPS_JPG_Q[qv // 5] args["optimize_coding"] = True elif tpath.endswith("jxl"): qv = vn.flags["th_qvx"] # args["effort"] = 8 # `- not worth it; twice as slow, size drops 12%, no visual improvement unlike ffmpeg img.write_to_file(tpath, Q=qv, strip=True, **args) img.invalidate() def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) def _loader(w: int, kw: dict) -> Any: return pyvips.Image.thumbnail(abspath, w, **kw) self.conv_image_vips(_loader, tpath, fmt, vn) def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) with rawpy.imread(abspath) as raw: thumb = raw.extract_thumb() if thumb.format == rawpy.ThumbFormat.JPEG and tpath.endswith(".jpg"): # if we have a jpg thumbnail and no webp output is available, # just write the jpg directly (it'll be the wrong size, but it's fast) with open(tpath, "wb") as f: f.write(thumb.data) if HAVE_VIPS: def _loader(w: int, kw: dict) -> Any: if thumb.format == rawpy.ThumbFormat.BITMAP: img = pyvips.Image.new_from_array(thumb.data, interpretation="rgb") return img.thumbnail_image(w, **kw) else: return pyvips.Image.thumbnail_buffer(thumb.data, w, **kw) self.conv_image_vips(_loader, tpath, fmt, vn) elif HAVE_PIL: if thumb.format == rawpy.ThumbFormat.BITMAP: im = Image.fromarray(thumb.data, "RGB") else: im = Image.open(io.BytesIO(thumb.data)) self.conv_image_pil(im, tpath, fmt, vn) else: raise Exception( "either pil or vips is needed to process embedded bitmap thumbnails in raw files" ) def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if not ret: return ext = abspath.rsplit(".")[-1].lower() if ext in ["h264", "h265"] or ext in self.fmt_ffi: seek: list[bytes] = [] else: dur = ret[".dur"][1] if ".dur" in ret else 4 seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0") def _ffmpeg_im( self, abspath: str, tpath: str, fmt: str, vn: VFS, seek: list[bytes], imap: bytes, ) -> None: scale = "scale={0}:{1}:force_original_aspect_ratio=" if "f" in fmt: scale += "decrease,setsar=1:1" else: scale += "increase,crop={0}:{1},setsar=1:1" res = self.getres(vn, fmt) bscale = scale.format(*list(res)).encode("utf-8") # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner" ] cmd += seek cmd += [ b"-i", fsenc(abspath), b"-map", imap, b"-vf", bscale, b"-frames:v", b"1", b"-metadata:s:v:0", b"rotate=0", ] # fmt: on self._ffmpeg_im_o(tpath, vn, cmd) def _ffmpeg_im_o(self, tpath: str, vn: VFS, cmd: list[bytes]) -> None: if tpath.endswith(".jpg"): cmd += [ b"-q:v", FF_JPG_Q[vn.flags["th_qv"] // 5], # default=?? ] elif tpath.endswith(".jxl"): cmd += [ b"-q:v", unicode(vn.flags["th_qvx"]).encode("ascii"), # default=?? b"-effort:v", b"8", # default=7, 1=fast, 9=max, 9~=8 but slower ] else: cmd += [ b"-q:v", unicode(vn.flags["th_qv"]).encode("ascii"), # default=75 b"-compression_level:v", b"6", # default=4, 0=fast, 6=max ] cmd += [fsenc(tpath)] self._run_ff(cmd, vn, "convt") def _run_ff(self, cmd: list[bytes], vn: VFS, kto: str, oom: int = 400) -> None: # self.log((b" ".join(cmd)).decode("utf-8")) ret, _, serr = runcmd(cmd, timeout=vn.flags[kto], nice=True, oom=oom) if not ret: return c: Union[str, int] = "90" t = "FFmpeg failed (probably a corrupt file):\n" if "but no decoder found for: hevc" in serr: t = "thumbnail cannot be created due to legal reasons; https://github.com/9001/copyparty/blob/hovudstraum/docs/bad-codecs.md \033[0;90m\n" ret = 321 c = 3 elif ( (not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60) and cmd[-1].lower().endswith(b".webp") and ( "Error selecting an encoder" in serr or "Automatic encoder selection failed" in serr or "Default encoder for format webp" in serr or "Please choose an encoder manually" in serr ) ): self.args.th_ff_jpg = time.time() t = "FFmpeg failed because it was compiled without libwebp; enabling --th-ff-jpg to force jpeg output:\n" ret = 321 c = 1 elif ( not self.args.th_ff_swr or time.time() - int(self.args.th_ff_swr) < 60 ) and ( "Requested resampling engine is unavailable" in serr or "output pad on Parsed_aresample_" in serr ): self.args.th_ff_swr = time.time() t = "FFmpeg failed because it was compiled without libsox; enabling --th-ff-swr to force swr resampling:\n" ret = 321 c = 1 lines = serr.strip("\n").split("\n") if len(lines) > 50: lines = lines[:25] + ["[...]"] + lines[-25:] txt = "\n".join(["ff: " + unicode(x) for x in lines]) if len(txt) > 5000: txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:] try: zs = shlex.join([x.decode("utf-8", "replace") for x in cmd]) except: zs = "'" + (b"' '".join(cmd)).decode("utf-8", "replace") + "'" self.log("%scmd: %s\n%s" % (t, zs, txt), c=c) raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in ret: raise Exception("not audio") # jt_versi.xm: 405M/839s dur = ret[".dur"][1] if ".dur" in ret else 300 need = 0.2 + dur / 3000 speedup = b"" if need > self.args.th_ram_max * 0.7: self.log("waves too big (need %.2f GiB); trying to optimize" % (need,)) need = 0.2 + dur / 4200 # only helps about this much... speedup = b"aresample=8000," if need > self.args.th_ram_max * 0.96: raise Exception("file too big; cannot waves") self.wait4ram(need, tpath) flt = b"[0:a:0]" + speedup flt += ( b"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2" b",volume=2" b",showwavespic=s=2048x64:colors=white" b",convolution=1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 1 1 1 1 1 1 1 1:1 -1 1 -1 5 -1 1 -1 1" # idk what im doing but it looks ok ) # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), b"-filter_complex", flt, b"-frames:v", b"1", ] # fmt: on cmd += [fsenc(tpath)] self._run_ff(cmd, vn, "convt") if "pngquant" in vn.flags: wtpath = tpath + ".png" cmd = [ b"pngquant", b"--strip", b"--nofs", b"--output", fsenc(wtpath), fsenc(tpath), ] ret = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=400)[0] if ret: try: wunlink(self.log, wtpath, vn.flags) except: pass else: atomic_move(self.log, wtpath, tpath, vn.flags) def conv_emb_cv( self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any] ) -> None: self.wait4ram(0.2, tpath) self._ffmpeg_im( abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii") ) def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in ret: raise Exception("not audio") want_spec = vn.flags.get("th_spec_p", 1) if want_spec < 2: for strm in strms: if ( strm.get("codec_type") == "video" and strm.get("DISPOSITION:attached_pic") == "1" ): return self.conv_emb_cv(abspath, tpath, fmt, vn, strm) if not want_spec: raise Exception("spectrograms forbidden by volflag") fext = abspath.split(".")[-1].lower() # https://trac.ffmpeg.org/ticket/10797 # expect 1 GiB every 600 seconds when duration is tricky; # simple filetypes are generally safer so let's special-case those coeff = 1800 if fext in EXTS_SPEC_SAFE else 600 dur = ret[".dur"][1] if ".dur" in ret else 900 need = 0.2 + dur / coeff self.wait4ram(need, tpath) infile = abspath if dur >= 900 or fext in self.exts_spec_unsafe: with tempfile.NamedTemporaryFile(suffix=".spec.flac", delete=False) as f: f.write(b"h") infile = f.name try: self.untemp[tpath].append(infile) except: self.untemp[tpath] = [infile] # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), b"-map", b"0:a:0", b"-ac", b"1", b"-ar", b"48000", b"-sample_fmt", b"s16", b"-t", b"900", b"-y", fsenc(infile), ] # fmt: on self._run_ff(cmd, vn, "convt") fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" if "3" in fmt: fc += "1280x1024,crop=1420:1056:70:48[o]" else: fc += "640x512,crop=780:544:70:48[o]" if self.args.th_ff_swr: fco = ":filter_size=128:cutoff=0.877" else: fco = ":resampler=soxr" fc = fc.format(fco) # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(infile), b"-filter_complex", fc.encode("utf-8"), b"-map", b"[o]", b"-frames:v", b"1", ] # fmt: on self._ffmpeg_im_o(tpath, vn, cmd) def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: quality = self.args.q_mp3.lower() if self.args.no_acode or not quality: raise Exception("disabled in server config") self.wait4ram(0.2, tpath) tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") if quality.endswith("k"): qk = b"-b:a" qv = quality.encode("ascii") else: qk = b"-q:a" qv = quality[1:].encode("ascii") # extremely conservative choices for output format # (always 2ch 44k1) because if a device is old enough # to not support opus then it's probably also super picky # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), ] + self.big_tags(rawtags) + [ b"-map", b"0:a:0", b"-ar", b"44100", b"-ac", b"2", b"-c:a", b"libmp3lame", qk, qv, fsenc(tpath) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: if self.args.no_acode or not self.args.allow_flac: raise Exception("flac not permitted in server config") self.wait4ram(0.2, tpath) tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") self.log("conv2 flac", 6) # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), b"-map", b"0:a:0", b"-c:a", b"flac", fsenc(tpath) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: if self.args.no_acode or not self.args.allow_wav: raise Exception("wav not permitted in server config") self.wait4ram(0.2, tpath) tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") bits = tags[".bps"][1] if bits == 0.0: bits = tags[".bprs"][1] codec = b"pcm_s32le" if bits <= 16.0: codec = b"pcm_s16le" elif bits <= 24.0: codec = b"pcm_s24le" self.log("conv2 wav", 6) # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), b"-map", b"0:a:0", b"-c:a", codec, fsenc(tpath) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: if self.args.no_acode or not self.args.q_opus: raise Exception("disabled in server config") self.wait4ram(0.2, tpath) tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") sq = "%dk" % (self.args.q_opus,) bq = sq.encode("ascii") if tags["ac"][1] == "opus": enc = "-c:a copy" else: enc = "-c:a libopus -b:a " + sq fun = self._conv_caf if fmt == "caf" else self._conv_owa fun(abspath, tpath, tags, rawtags, enc, bq, vn) def _conv_owa( self, abspath: str, tpath: str, tags: dict[str, tuple[int, Any]], rawtags: dict[str, list[Any]], enc: str, bq: bytes, vn: VFS, ) -> None: if tpath.endswith(".owa"): container = b"webm" tagset = [b"-map_metadata", b"-1"] else: container = b"opus" tagset = self.big_tags(rawtags) self.log("conv2 %s [%s]" % (container, enc), 6) benc = enc.encode("ascii").split(b" ") # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), ] + tagset + [ b"-map", b"0:a:0", ] + benc + [ b"-f", container, fsenc(tpath) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) def _conv_caf( self, abspath: str, tpath: str, tags: dict[str, tuple[int, Any]], rawtags: dict[str, list[Any]], enc: str, bq: bytes, vn: VFS, ) -> None: tmp_opus = tpath + ".opus" try: wunlink(self.log, tmp_opus, vn.flags) except: pass try: dur = tags[".dur"][1] except: dur = 0 self.log("conv2 caf-tmp [%s]" % (enc,), 6) benc = enc.encode("ascii").split(b" ") # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), b"-map_metadata", b"-1", b"-map", b"0:a:0", ] + benc + [ b"-f", b"opus", fsenc(tmp_opus) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) # iOS fails to play some "insufficiently complex" files # (average file shorter than 8 seconds), so of course we # fix that by mixing in some inaudible pink noise :^) # 6.3 sec seems like the cutoff so lets do 7, and # 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB sz = bos.path.getsize(tmp_opus) if dur < 20 or sz < 256 * 1024: zs = bq.decode("ascii") self.log("conv2 caf-transcode; dur=%d sz=%d q=%s" % (dur, sz, zs), 6) # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(abspath), b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix", b"-map_metadata", b"-1", b"-ac", b"2", b"-c:a", b"libopus", b"-b:a", bq, b"-f", b"caf", fsenc(tpath) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) else: # simple remux should be safe self.log("conv2 caf-remux; dur=%d sz=%d" % (dur, sz), 6) # fmt: off cmd = [ b"ffmpeg", b"-nostdin", b"-v", b"error", b"-hide_banner", b"-i", fsenc(tmp_opus), b"-map_metadata", b"-1", b"-map", b"0:a:0", b"-c:a", b"copy", b"-f", b"caf", fsenc(tpath) ] # fmt: on self._run_ff(cmd, vn, "aconvt", oom=300) try: wunlink(self.log, tmp_opus, vn.flags) except: pass def big_tags(self, raw_tags: dict[str, list[str]]) -> list[bytes]: ret = [] for k, vs in raw_tags.items(): for v in vs: if len(unicode(v)) >= 1024: bv = k.encode("utf-8", "replace") ret += [b"-metadata", bv + b"="] break return ret def poke(self, tdir: str) -> None: if not self.poke_cd.poke(tdir): return ts = int(time.time()) try: for _ in range(4): bos.utime(tdir, (ts, ts)) tdir = os.path.dirname(tdir) except: pass def cleaner(self) -> None: interval = self.args.th_clean while True: ndirs = 0 for vol, histpath in self.asrv.vfs.histtab.items(): if histpath.startswith(vol): self.log("\033[Jcln {}/\033[A".format(histpath)) else: self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol)) try: ndirs += self.clean(histpath) except Exception as ex: self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3) self.log("\033[Jcln ok; rm {} dirs".format(ndirs)) self.rm_nullthumbs = False time.sleep(interval) def clean(self, histpath: str) -> int: cfgi = cfga = "" for vn in self.asrv.vfs.all_vols.values(): if vn.histpath == histpath: cfgi = self.volcfgi(vn) cfga = self.volcfga(vn) break for cfg, cat in ((cfgi, "th"), (cfga, "ac")): if not cfg: continue try: with open(os.path.join(histpath, cat, "cfg.txt"), "rb") as f: oldcfg = f.read().decode("utf-8") except: oldcfg = "" if cfg == oldcfg: continue zs = os.path.join(histpath, cat) if not os.path.exists(zs): continue self.log("thumbnailer-config changed; deleting %s" % (zs,), 3) shutil.rmtree(zs) ret = 0 for cat in ["th", "ac"]: top = os.path.join(histpath, cat) if not bos.path.isdir(top): continue ret += self._clean(cat, top) return ret def _clean(self, cat: str, thumbpath: str) -> int: # self.log("cln {}".format(thumbpath)) exts = EXTS_TH if cat == "th" else EXTS_AC maxage = getattr(self.args, cat + "_maxage") now = time.time() prev_b64 = None prev_fp = "" try: t1 = statdir( self.log_func, not self.args.no_scandir, False, thumbpath, False ) ents = sorted(list(t1)) except: return 0 ndirs = 0 for f, inf in ents: fp = os.path.join(thumbpath, f) cmp = fp.lower().replace("\\", "/") # "top" or b64 prefix/full (a folder) if len(f) <= 3 or len(f) == 24: age = now - inf.st_mtime if age > maxage: with self.mutex: safe = True for k in self.busy: if k.lower().replace("\\", "/").startswith(cmp): safe = False break if safe: ndirs += 1 self.log("rm -rf [{}]".format(fp)) shutil.rmtree(fp, ignore_errors=True) else: ndirs += self._clean(cat, fp) continue # thumb file try: b64, ts, ext = f.split(".") if len(ts) > 8 and PTN_TS.match(ts): ts = "yeahokay" if len(b64) != 24 or len(ts) != 8 or ext not in exts: raise Exception() except: if f != "dir.txt" and f != "cfg.txt": self.log("foreign file in thumbs dir: [{}]".format(fp), 1) continue if self.rm_nullthumbs and not inf.st_size: bos.unlink(fp) continue if b64 == prev_b64: self.log("rm replaced [{}]".format(fp)) bos.unlink(prev_fp) if cat != "th" and inf.st_mtime + maxage < now: self.log("rm expired [{}]".format(fp)) bos.unlink(fp) prev_b64 = b64 prev_fp = fp return ndirs ================================================ FILE: copyparty/u2idx.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import calendar import os import re import threading import time from operator import itemgetter from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .authsrv import LEELOO_DALLAS, VFS from .bos import bos from .up2k import up2k_wark_from_hashlist from .util import ( HAVE_SQLITE3, Daemon, Pebkac, absreal, gen_filekey, min_ex, quotep, s3dec, vjoin, ) if HAVE_SQLITE3: import sqlite3 try: from pathlib import Path except: pass if True: # pylint: disable=using-constant-test from typing import Any, Optional, Union if TYPE_CHECKING: from .httpsrv import HttpSrv if PY2: range = xrange # type: ignore class U2idx(object): def __init__(self, hsrv: "HttpSrv") -> None: self.log_func = hsrv.log self.asrv = hsrv.asrv self.args = hsrv.args self.timeout = self.args.srch_time if not HAVE_SQLITE3: self.log("your python does not have sqlite3; searching will be disabled") return if self.args.srch_icase: self._open_db = self._open_db_icase else: self._open_db = self._open_db_std assert sqlite3 # type: ignore # !rm self.active_id = "" self.active_cur: Optional["sqlite3.Cursor"] = None self.cur: dict[str, "sqlite3.Cursor"] = {} self.mem_cur = sqlite3.connect(":memory:", check_same_thread=False).cursor() self.mem_cur.execute(r"create table a (b text)") self.sh_cur: Optional["sqlite3.Cursor"] = None self.p_end = 0.0 self.p_dur = 0.0 def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func("u2idx", msg, c) def _open_db_std(self, *args, **kwargs): assert sqlite3 # type: ignore # !rm kwargs["check_same_thread"] = False return sqlite3.connect(*args, **kwargs) def _open_db_icase(self, *args, **kwargs): db = self._open_db_std(*args, **kwargs) db.create_function("casefold", 1, lambda x: x.casefold() if x else x) return db def shutdown(self) -> None: if not HAVE_SQLITE3: return for cur in self.cur.values(): db = cur.connection try: db.interrupt() except: pass cur.close() db.close() for cur in (self.mem_cur, self.sh_cur): if cur: db = cur.connection cur.close() db.close() def fsearch( self, uname: str, vols: list[VFS], body: dict[str, Any] ) -> list[dict[str, Any]]: """search by up2k hashlist""" if not HAVE_SQLITE3: return [] fsize = body["size"] fhash = body["hash"] wark = up2k_wark_from_hashlist(self.args.warksalt, fsize, fhash) uq = "substr(w,1,16) = ? and w = ?" uv: list[Union[str, int]] = [wark[:16], wark] try: return self.run_query(uname, vols, uq, uv, False, True, 99999)[0] except: raise Pebkac(500, min_ex()) def get_shr(self) -> Optional["sqlite3.Cursor"]: if self.sh_cur: return self.sh_cur if not HAVE_SQLITE3 or not self.args.shr: return None assert sqlite3 # type: ignore # !rm db = sqlite3.connect(self.args.shr_db, timeout=2, check_same_thread=False) cur = db.cursor() cur.execute('pragma table_info("sh")').fetchall() self.sh_cur = cur return cur def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]: cur = self.cur.get(vn.realpath) if cur: return cur if not HAVE_SQLITE3 or "e2d" not in vn.flags: return None assert sqlite3 # type: ignore # !rm ptop = vn.realpath histpath = self.asrv.vfs.dbpaths.get(ptop) if not histpath: self.log("no dbpath for %r" % (ptop,)) return None db_path = os.path.join(histpath, "up2k.db") if not bos.path.exists(db_path): return None cur = None if ANYWIN and not bos.path.exists(db_path + "-wal"): uri = "" try: uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri()) cur = self._open_db(uri, timeout=2, uri=True).cursor() cur.execute('pragma table_info("up")').fetchone() self.log("ro: %r" % (db_path,)) except: self.log("could not open read-only: {}\n{}".format(uri, min_ex())) # may not fail until the pragma so unset it cur = None if not cur: # on windows, this steals the write-lock from up2k.deferred_init -- # seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2 cur = self._open_db(db_path, timeout=2).cursor() self.log("opened %r" % (db_path,)) self.cur[ptop] = cur return cur def search( self, uname: str, vols: list[VFS], uq: str, lim: int ) -> tuple[list[dict[str, Any]], list[str], bool]: """search by query params""" if not HAVE_SQLITE3: return [], [], False icase = self.args.srch_icase q = "" v: Union[str, int] = "" va: list[Union[str, int]] = [] have_mt = False is_key = True is_size = False is_date = False is_wark = False field_end = "" # closing parenthesis or whatever kw_key = ["(", ")", "and ", "or ", "not "] kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "] ptn_mt = re.compile(r"^\.?[a-z_-]+$") ptn_lc = re.compile(r" (mt\.v) ([=]+) \? \) $") ptn_lcv = re.compile(r"[a-zA-Z]") while True: uq = uq.strip() if not uq: break ok = False for kw in kw_key + kw_val: if uq.startswith(kw): is_key = kw in kw_key uq = uq[len(kw) :] ok = True if is_wark: kw = "= " q += kw break if ok: continue if uq.startswith('"'): v, uq = uq[1:].split('"', 1) while v.endswith("\\"): v2, uq = uq.split('"', 1) v = v[:-1] + '"' + v2 uq = uq.strip() else: v, uq = (uq + " ").split(" ", 1) v = v.replace('\\"', '"') if is_key: is_key = False if v == "size": v = "up.sz" is_size = True elif v == "date": v = "up.mt" is_date = True elif v == "up_at": v = "up.at" is_date = True elif v == "path": v = "trim(?||up.rd,'/')" va.append("\nrd") if icase: v = "casefold(%s)" % (v,) elif v == "name": v = "up.fn" if icase: v = "casefold(%s)" % (v,) elif v == "w": v = "substr(up.w,1,16)" is_wark = True elif v == "tags" or ptn_mt.match(v): have_mt = True field_end = ") " if v == "tags": vq = "mt.v" else: vq = "+mt.k = '{}' and mt.v".format(v) v = "exists(select 1 from mt where mt.w = mtw and " + vq else: raise Pebkac(400, "invalid key %r" % (v,)) q += v + " " continue head = "" tail = "" if is_date: is_date = False v = re.sub(r"[tzTZ, ]+", " ", v).strip() for fmt in [ "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d %H", "%Y-%m-%d", "%Y-%m", "%Y", ]: try: v = calendar.timegm(time.strptime(str(v), fmt)) break except: pass elif is_size: is_size = False v = int(float(v) * 1024 * 1024) elif is_wark: is_wark = False v = v.strip("*") if len(v) > 16: v = v[:16] if len(v) < 16: raise Pebkac(400, "w/filehash must be 16+ chars") else: if v.startswith("*"): head = "'%'||" v = v[1:] if v.endswith("*"): tail = "||'%'" v = v[:-1] if icase and "casefold(" in q: try: v = unicode(v).casefold() except: v = unicode(v).lower() q += " {}?{} ".format(head, tail) va.append(v) is_key = True if field_end: q += field_end field_end = "" # lowercase tag searches m = ptn_lc.search(q) zs = unicode(v) if not m or not ptn_lcv.search(zs): continue va.pop() va.append(zs.lower()) q = q[: m.start()] field, oper = m.groups() if oper in ["=", "=="]: q += " {} like ? ) ".format(field) else: q += " lower({}) {} ? ) ".format(field, oper) try: return self.run_query(uname, vols, q, va, have_mt, True, lim) except Exception as ex: raise Pebkac(500, repr(ex)) def run_query( self, uname: str, vols: list[VFS], uq: str, uv: Union[list[str], list[Union[str, int]]], have_mt: bool, sort: bool, lim: int, ) -> tuple[list[dict[str, Any]], list[str], bool]: dbg = self.args.srch_dbg if dbg: t = "searching across all %s volumes in which the user has 'r' (full read access):\n %s" zs = "\n ".join(["/%s = %s" % (x.vpath, x.realpath) for x in vols]) self.log(t % (len(vols), zs), 5) done_flag: list[bool] = [] self.active_id = "{:.6f}_{}".format( time.time(), threading.current_thread().ident ) Daemon(self.terminator, "u2idx-terminator", (self.active_id, done_flag)) if not uq or not uv: uq = "select * from up" uv = [] elif have_mt: uq = "select up.*, substr(up.w,1,16) mtw from up where " + uq else: uq = "select up.* from up where " + uq self.log("qs: {!r} {!r}".format(uq, uv)) ret = [] seen_rps: set[str] = set() clamp = int(self.args.srch_hits) if lim >= clamp: lim = clamp clamped = True else: clamped = False taglist = {} for vol in vols: if lim < 0: break vtop = vol.vpath ptop = vol.realpath flags = vol.flags cur = self.get_cur(vol) if not cur: continue dots = flags.get("dotsrch") and uname in vol.axs.udot zs = "srch_re_dots" if dots else "srch_re_nodot" rex: re.Pattern = flags.get(zs) # type: ignore if dbg: t = "searching in volume /%s (%s), excluding %s" self.log(t % (vtop, ptop, rex.pattern), 5) rex_cfg: Optional[re.Pattern] = flags.get("srch_excl") self.active_cur = cur vuv = [] for v in uv: if v == "\nrd": v = vtop + "/" vuv.append(v) sret = [] fk = flags.get("fk") fk_alg = 2 if "fka" in flags else 1 c = cur.execute(uq, tuple(vuv)) for hit in c: w, ts, sz, rd, fn = hit[:5] if rd.startswith("//") or fn.startswith("//"): rd, fn = s3dec(rd, fn) vp = vjoin(vjoin(vtop, rd), fn) if vp in seen_rps: continue if rex.search(vp): if dbg: if rex_cfg and rex_cfg.search(vp): # type: ignore self.log("filtered by srch_excl: %s" % (vp,), 6) elif not dots and "/." in ("/" + vp): pass else: t = "database inconsistency in volume '/%s'; ignoring: %s" self.log(t % (vtop, vp), 1) continue rp = quotep(vp) if not fk: suf = "" else: try: ap = absreal(os.path.join(ptop, rd, fn)) ino = 0 if ANYWIN or fk_alg == 2 else bos.stat(ap).st_ino except: continue suf = "?k=" + gen_filekey( fk_alg, self.args.fk_salt, ap, sz, ino, )[:fk] lim -= 1 if lim < 0: break if dbg: t = "in volume '/%s': hit: %s" self.log(t % (vtop, rp), 5) zs = vjoin(vtop, rp) chk_vn, _ = self.asrv.vfs.get(zs, LEELOO_DALLAS, True, False) chk_vn = chk_vn.dbv or chk_vn if chk_vn.vpath != vtop: raise Exception( "database inconsistency! in volume '/%s' (%s), found file [%s] which belongs to volume '/%s' (%s)" % (vtop, ptop, zs, chk_vn.vpath, chk_vn.realpath) ) seen_rps.add(rp) sret.append({"ts": int(ts), "sz": sz, "rp": rp + suf, "w": w[:16]}) for hit in sret: w = hit["w"] del hit["w"] tags = {} q2 = "select k, v from mt where w = ? and +k != 'x'" for k, v2 in cur.execute(q2, (w,)): taglist[k] = True tags[k] = v2 hit["tags"] = tags ret.extend(sret) # print("[{}] {}".format(ptop, sret)) if dbg: t = "in volume '/%s': got %d hits, %d total so far" self.log(t % (vtop, len(sret), len(ret)), 5) done_flag.append(True) self.active_id = "" if sort: ret.sort(key=itemgetter("rp")) return ret, list(taglist.keys()), lim < 0 and not clamped def terminator(self, identifier: str, done_flag: list[bool]) -> None: for _ in range(self.timeout): time.sleep(1) if done_flag: return if identifier == self.active_id: assert self.active_cur # !rm self.active_cur.connection.interrupt() ================================================ FILE: copyparty/up2k.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import errno import hashlib import json import math import os import re import shutil import stat import subprocess as sp import sys import tempfile import threading import time import traceback from copy import deepcopy from queue import Queue from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv from .bos import bos from .cfg import vf_bmap, vf_cmap, vf_vmap from .fsutil import Fstab from .mtag import MParser, MTag from .util import ( E_FS_CRIT, E_FS_MEH, HAVE_FICLONE, HAVE_SQLITE3, SYMTIME, VF_CAREFUL, Daemon, MTHash, Pebkac, ProgressPrinter, absreal, alltrace, atomic_move, db_ex_chk, dir_is_empty, djoin, fsenc, gen_filekey, gen_filekey_dbg, gzip, hidedir, humansize, min_ex, pathmod, quotep, rand_name, ren_open, rmdirs, rmdirs_up, runhook, runihook, s2hms, s3dec, s3enc, sanitize_fn, set_ap_perms, set_fperms, sfsenc, spack, statdir, trystat_shutil_copy2, ub64enc, unhumanize, vjoin, vsplit, w8b64dec, w8b64enc, wunlink, ) try: from pathlib import Path except: pass if HAVE_SQLITE3: import sqlite3 DB_VER = 6 if True: # pylint: disable=using-constant-test from typing import Any, Optional, Pattern, Union if TYPE_CHECKING: from .svchub import SvcHub USE_FICLONE = HAVE_FICLONE and sys.version_info < (3, 14) if USE_FICLONE: import fcntl zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp" ICV_EXTS = set(zsg.split(",")) zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv" VCV_EXTS = set(zsg.split(",")) zsg = "aif,aiff,alac,ape,flac,m4a,m4b,m4r,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub" ACV_EXTS = set(zsg.split(",")) zsg = "nohash noidx xdev xvol" VF_AFFECTS_INDEXING = set(zsg.split(" ")) SBUSY = "cannot receive uploads right now;\nserver busy with %s.\nPlease wait; the client will retry..." HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume), or, if you want to keep the thumbnails in the current location and only move the database itself, then use --dbpath or volflag dbpath" NULLSTAT = os.stat_result((0, -1, -1, 0, 0, 0, 0, 0, 0, 0)) class Dbw(object): def __init__(self, c: "sqlite3.Cursor", n: int, nf: int, t: float) -> None: self.c = c self.n = n self.nf = nf self.t = t class Mpqe(object): """pending files to tag-scan""" def __init__( self, mtp: dict[str, MParser], entags: set[str], vf: dict[str, Any], w: str, abspath: str, oth_tags: dict[str, Any], ): # mtp empty = mtag self.mtp = mtp self.entags = entags self.vf = vf self.w = w self.abspath = abspath self.oth_tags = oth_tags class Up2k(object): def __init__(self, hub: "SvcHub") -> None: self.hub = hub self.asrv: AuthSrv = hub.asrv self.args = hub.args self.log_func = hub.log self.vfs = self.asrv.vfs self.acct = self.asrv.acct self.iacct = self.asrv.iacct self.grps = self.asrv.grps self.salt = self.args.warksalt self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") self.abrt_key = "" self.gid = 0 self.gt0 = 0 self.gt1 = 0 self.stop = False self.fika = "" self.mutex = threading.Lock() self.reload_mutex = threading.Lock() self.reload_flag = 0 self.reloading = False self.blocked: Optional[str] = None self.pp: Optional[ProgressPrinter] = None self.rescan_cond = threading.Condition() self.need_rescan: set[str] = set() self.db_act = 0.0 self.reg_mutex = threading.Lock() self.registry: dict[str, dict[str, dict[str, Any]]] = {} self.flags: dict[str, dict[str, Any]] = {} self.droppable: dict[str, list[str]] = {} self.volnfiles: dict["sqlite3.Cursor", int] = {} self.volsize: dict["sqlite3.Cursor", int] = {} self.volstate: dict[str, str] = {} self.vol_act: dict[str, float] = {} self.busy_aps: dict[str, int] = {} self.dupesched: dict[str, list[tuple[str, str, float]]] = {} self.snap_prev: dict[str, Optional[tuple[int, float]]] = {} self.mtag: Optional[MTag] = None self.entags: dict[str, set[str]] = {} self.mtp_parsers: dict[str, dict[str, MParser]] = {} self.pending_tags: list[tuple[set[str], str, str, dict[str, Any]]] = [] self.hashq: Queue[ tuple[str, str, dict[str, Any], str, str, str, float, str, bool] ] = Queue() self.tagq: Queue[tuple[str, str, str, str, int, str, float]] = Queue() self.tag_event = threading.Condition() self.hashq_mutex = threading.Lock() self.n_hashq = 0 self.n_tagq = 0 self.mpool_used = False self.xiu_ptn = re.compile(r"(?:^|,)i([0-9]+)") self.xiu_busy = False # currently running hook self.xiu_asleep = True # needs rescan_cond poke to schedule self self.fx_backlog: list[tuple[str, dict[str, str], str]] = [] self.cur: dict[str, "sqlite3.Cursor"] = {} self.mem_cur = None self.sqlite_ver = None self.no_expr_idx = False self.timeout = int(max(self.args.srch_time, 50) * 1.2) + 1 self.spools: set[tempfile.SpooledTemporaryFile[bytes]] = set() if HAVE_SQLITE3: # mojibake detector assert sqlite3 # type: ignore # !rm self.mem_cur = self._orz(":memory:") self.mem_cur.execute(r"create table a (b text)") self.sqlite_ver = tuple([int(x) for x in sqlite3.sqlite_version.split(".")]) if self.sqlite_ver < (3, 9): self.no_expr_idx = True else: t = "could not initialize sqlite3, will use in-memory registry only" self.log(t, 3) self.fstab = Fstab(self.log_func, self.args, True) self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey if self.args.hash_mt < 2: self.mth: Optional[MTHash] = None else: self.mth = MTHash(self.args.hash_mt) if self.args.no_fastboot: self.deferred_init() def init_vols(self) -> None: if self.args.no_fastboot: return Daemon(self.deferred_init, "up2k-deferred-init") def unpp(self) -> None: self.gt1 = time.time() if self.pp: self.pp.end = True self.pp = None def reload(self, rescan_all_vols: bool) -> None: n = 2 if rescan_all_vols else 1 with self.reload_mutex: if self.reload_flag < n: self.reload_flag = n with self.rescan_cond: self.rescan_cond.notify_all() def _reload_thr(self) -> None: while self.pp: time.sleep(0.1) while True: with self.reload_mutex: if not self.reload_flag: break rav = self.reload_flag == 2 self.reload_flag = 0 gt1 = self.gt1 with self.mutex: self._reload(rav) while gt1 == self.gt1 or self.pp: time.sleep(0.1) self.reloading = False def _reload(self, rescan_all_vols: bool) -> None: """mutex(main) me""" self.log("reload #{} scheduled".format(self.gid + 1)) all_vols = self.asrv.vfs.all_vols with self.reg_mutex: scan_vols = [ k for k, v in all_vols.items() if v.realpath not in self.registry ] if rescan_all_vols: scan_vols = list(all_vols.keys()) self._rescan(all_vols, scan_vols, True, False) def deferred_init(self) -> None: all_vols = self.asrv.vfs.all_vols have_e2d = self.init_indexes(all_vols, [], False) if self.stop: # up-mt consistency not guaranteed if init is interrupted; # drop caches for a full scan on next boot with self.mutex, self.reg_mutex: self._drop_caches() self.unpp() return if not self.pp and self.args.exit == "idx": return self.hub.sigterm() if self.hub.is_dut: return Daemon(self._snapshot, "up2k-snapshot") if have_e2d: Daemon(self._hasher, "up2k-hasher") Daemon(self._sched_rescan, "up2k-rescan") if self.mtag: for n in range(max(1, self.args.mtag_mt)): Daemon(self._tagger, "tagger-{}".format(n)) def log(self, msg: str, c: Union[int, str] = 0) -> None: if self.pp: msg += "\033[K" self.log_func("up2k", msg, c) def _gen_fk(self, alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str: return gen_filekey_dbg( alg, salt, fspath, fsize, inode, self.log, self.args.log_fk ) def _block(self, why: str) -> None: self.blocked = why self.log("uploads temporarily blocked due to " + why, 3) def _unblock(self) -> None: if self.blocked is not None: self.blocked = None if not self.stop: self.log("uploads are now possible", 2) def get_state(self, get_q: bool, uname: str) -> str: mtpq: Union[int, str] = 0 ups = [] up_en = not self.args.no_up_list q = "select count(w) from mt where k = 't:mtp'" got_lock = False if PY2 else self.mutex.acquire(timeout=0.5) if got_lock: try: for cur in self.cur.values() if get_q else []: try: mtpq += cur.execute(q).fetchone()[0] except: pass if uname and up_en: ups = self._active_uploads(uname) finally: self.mutex.release() else: mtpq = "(?)" if up_en: ups = [(1, 0, 0, time.time(), "cannot show list (server too busy)")] if PY2: ups = [] ups.sort(reverse=True) ret = { "volstate": self.volstate, "scanning": bool(self.pp), "hashq": self.n_hashq, "tagq": self.n_tagq, "mtpq": mtpq, "ups": ups, "dbwu": "{:.2f}".format(self.db_act), "dbwt": "{:.2f}".format( min(1000 * 24 * 60 * 60 - 1, time.time() - self.db_act) ), } return json.dumps(ret, separators=(",\n", ": ")) def _active_uploads(self, uname: str) -> list[tuple[float, int, int, str]]: ret = [] for vtop in self.vfs.aread.get(uname) or []: vfs = self.vfs.all_vols.get(vtop) if not vfs: # dbv only continue ptop = vfs.realpath tab = self.registry.get(ptop) if not tab: continue for job in tab.values(): ineed = len(job["need"]) ihash = len(job["hash"]) if ineed == ihash or not ineed: continue poke = job["poke"] zt = ( ineed / ihash, job["size"], int(job.get("t0c", poke)), int(poke), djoin(vtop, job["prel"], job["name"]), ) ret.append(zt) return ret def find_job_by_ap(self, ptop: str, ap: str) -> str: try: if ANYWIN: ap = ap.replace("\\", "/") vp = ap[len(ptop) :].strip("/") dn, fn = vsplit(vp) with self.reg_mutex: tab2 = self.registry[ptop] for job in tab2.values(): if job["prel"] == dn and job["name"] == fn: return json.dumps(job, separators=(",\n", ": ")) except: pass return "{}" def get_unfinished_by_user(self, uname, ip) -> dict[str, Any]: # returns dict due to ExceptionalQueue if PY2 or not self.reg_mutex.acquire(timeout=2): return {"timeout": 1} ret: list[tuple[int, str, int, int, int]] = [] userset = set([(uname or "\n"), "*"]) e_d = {} n = 1000 try: for ptop, tab2 in self.registry.items(): cfg = self.flags.get(ptop, e_d).get("u2abort", 1) if not cfg: continue addr = (ip or "\n") if cfg in (1, 2) else "" user = userset if cfg in (1, 3) else None for job in tab2.values(): if ( "done" in job or (user and job["user"] not in user) or (addr and addr != job["addr"]) ): continue zt5 = ( int(job["t0"]), djoin(job["vtop"], job["prel"], job["name"]), job["size"], len(job["need"]), len(job["hash"]), ) ret.append(zt5) n -= 1 if not n: break finally: self.reg_mutex.release() if ANYWIN: ret = [(x[0], x[1].replace("\\", "/"), x[2], x[3], x[4]) for x in ret] ret.sort(reverse=True) ret2 = [ { "at": at, "vp": "/" + quotep(vp), "pd": 100 - ((nn * 100) // (nh or 1)), "sz": sz, } for (at, vp, sz, nn, nh) in ret ] return {"f": ret2} def get_unfinished(self) -> str: if PY2 or not self.reg_mutex.acquire(timeout=0.5): return "" ret: dict[str, tuple[int, int]] = {} try: for ptop, tab2 in self.registry.items(): nbytes = 0 nfiles = 0 for job in tab2.values(): if "done" in job: continue nfiles += 1 try: # close enough on average nbytes += len(job["need"]) * job["size"] // len(job["hash"]) except: pass ret[ptop] = (nbytes, nfiles) finally: self.reg_mutex.release() return json.dumps(ret, separators=(",\n", ": ")) def get_volsize(self, ptop: str) -> tuple[int, int]: with self.reg_mutex: return self._get_volsize(ptop) def get_volsizes(self, ptops: list[str]) -> list[tuple[int, int]]: ret = [] with self.reg_mutex: for ptop in ptops: ret.append(self._get_volsize(ptop)) return ret def _get_volsize(self, ptop: str) -> tuple[int, int]: if "e2ds" not in self.flags.get(ptop, {}): return (0, 0) cur = self.cur[ptop] nbytes = self.volsize[cur] nfiles = self.volnfiles[cur] for j in list(self.registry.get(ptop, {}).values()): nbytes += j["size"] nfiles += 1 return (nbytes, nfiles) def rescan( self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool ) -> str: with self.mutex: return self._rescan(all_vols, scan_vols, wait, fscan) def _rescan( self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool ) -> str: """mutex(main) me""" if not wait and self.pp: return "cannot initiate; scan is already in progress" self.gid += 1 Daemon( self.init_indexes, "up2k-rescan-{}".format(scan_vols[0] if scan_vols else "all"), (all_vols, scan_vols, fscan, self.gid), ) return "" def _sched_rescan(self) -> None: volage = {} cooldown = timeout = time.time() + 3.0 while not self.stop: now = time.time() timeout = max(timeout, cooldown) wait = timeout - time.time() # self.log("SR in {:.2f}".format(wait), 5) with self.rescan_cond: self.rescan_cond.wait(wait) if self.stop: return with self.reload_mutex: if self.reload_flag and not self.reloading: self.reloading = True zs = "up2k-reload-%d" % (self.gid,) Daemon(self._reload_thr, zs) now = time.time() if now < cooldown: # self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5) timeout = cooldown # wakeup means stuff to do, forget timeout continue if self.pp: # self.log("SR: pp; cd := 1", 5) cooldown = now + 1 continue cooldown = now + 3 # self.log("SR", 5) if self.args.no_lifetime and not self.args.shr: timeout = now + 9001 else: # important; not deferred by db_act timeout = self._check_lifetimes() timeout = min(self._check_forget_ip(), timeout) try: if self.args.shr: timeout = min(self._check_shares(), timeout) except Exception as ex: timeout = min(timeout, now + 60) t = "could not check for expiring shares: %r" self.log(t % (ex,), 1) try: timeout = min(timeout, now + self._check_xiu()) except Exception as ex: if "closed cursor" in str(ex): self.log("sched_rescan: lost db") return raise with self.mutex: for vp, vol in sorted(self.vfs.all_vols.items()): maxage = vol.flags.get("scan") if not maxage: continue if vp not in volage: volage[vp] = now deadline = volage[vp] + maxage if deadline <= now: self.need_rescan.add(vp) timeout = min(timeout, deadline) if self.db_act > now - self.args.db_act and self.need_rescan: # recent db activity; defer volume rescan act_timeout = self.db_act + self.args.db_act if self.need_rescan: timeout = now if timeout < act_timeout: timeout = act_timeout t = "volume rescan deferred {:.1f} sec, due to database activity" self.log(t.format(timeout - now)) continue with self.mutex: vols = list(sorted(self.need_rescan)) self.need_rescan.clear() if vols: cooldown = now + 10 err = self.rescan(self.vfs.all_vols, vols, False, False) if err: for v in vols: self.need_rescan.add(v) continue for v in vols: volage[v] = now def _check_forget_ip(self) -> float: now = time.time() timeout = now + 9001 for vp, vol in sorted(self.vfs.all_vols.items()): maxage = vol.flags["forget_ip"] if not maxage: continue cur = self.cur.get(vol.realpath) if not cur: continue cutoff = now - maxage * 60 for _ in range(2): q = "select ip, at from up where ip > '' order by +at limit 1" with self.mutex: hits = cur.execute(q).fetchall() if not hits: break remains = hits[0][1] - cutoff if remains > 0: timeout = min(timeout, now + remains) break q = "update up set ip = '' where ip > '' and at <= %d" cur.execute(q % (cutoff,)) zi = cur.rowcount cur.connection.commit() t = "forget-ip(%d) removed %d IPs from db [/%s]" self.log(t % (maxage, zi, vol.vpath)) timeout = min(timeout, now + 900) return timeout def _check_lifetimes(self) -> float: now = time.time() timeout = now + 9001 for vp, vol in sorted(self.vfs.all_vols.items()): lifetime = vol.flags.get("lifetime") if not lifetime: continue cur = self.cur.get(vol.realpath) if not cur: continue nrm = 0 deadline = time.time() - lifetime timeout = min(timeout, now + lifetime) q = "select rd, fn from up where at > 0 and at < ? limit 100" while True: with self.mutex: hits = cur.execute(q, (deadline,)).fetchall() if not hits: break for rd, fn in hits: if rd.startswith("//") or fn.startswith("//"): rd, fn = s3dec(rd, fn) fvp = ("%s/%s" % (rd, fn)).strip("/") if vp: fvp = "%s/%s" % (vp, fvp) self._handle_rm(LEELOO_DALLAS, "", fvp, [], True, False) nrm += 1 if nrm: self.log("%d files graduated in /%s" % (nrm, vp)) if timeout < 10: continue q = "select at from up where at > 0 order by at limit 1" with self.mutex: hits = cur.execute(q).fetchone() if hits: timeout = min(timeout, now + lifetime - (now - hits[0])) return timeout def _check_shares(self) -> float: assert sqlite3 # type: ignore # !rm now = time.time() timeout = now + 9001 maxage = self.args.shr_rt * 60 low = now - maxage vn = self.vfs.nodes.get(self.args.shr.strip("/")) active = vn and vn.nodes db = sqlite3.connect(self.args.shr_db, timeout=2) cur = db.cursor() q = "select k from sh where t1 and t1 <= ?" rm = [x[0] for x in cur.execute(q, (now,))] if active else [] if rm: assert vn and vn.nodes # type: ignore # self.log("chk_shr: %d" % (len(rm),)) zss = set(rm) rm = [zs for zs in vn.nodes if zs in zss] reload = bool(rm) if reload: self.log("disabling expired shares %s" % (rm,)) rm = [x[0] for x in cur.execute(q, (low,))] if rm: self.log("forgetting expired shares %s" % (rm,)) cur.executemany("delete from sh where k=?", [(x,) for x in rm]) cur.executemany("delete from sf where k=?", [(x,) for x in rm]) db.commit() if reload: Daemon(self.hub.reload, "sharedrop", (False, False)) q = "select min(t1) from sh where t1 > ?" (earliest,) = cur.execute(q, (1,)).fetchone() if earliest: # deadline for revoking regular access timeout = min(timeout, earliest + maxage) (earliest,) = cur.execute(q, (now - 2,)).fetchone() if earliest: # deadline for revival; drop entirely timeout = min(timeout, earliest) cur.close() db.close() if self.args.shr_v: self.log("next shr_chk = %d (%d)" % (timeout, timeout - time.time())) return timeout def _check_xiu(self) -> float: if self.xiu_busy: return 2 ret = 9001 for _, vol in sorted(self.vfs.all_vols.items()): rp = vol.realpath cur = self.cur.get(rp) if not cur: continue with self.mutex: q = "select distinct c from iu" cds = cur.execute(q).fetchall() if not cds: continue run_cds: list[int] = [] for cd in sorted([x[0] for x in cds]): delta = cd - (time.time() - self.vol_act[rp]) if delta > 0: ret = min(ret, delta) break run_cds.append(cd) if run_cds: self.xiu_busy = True Daemon(self._run_xius, "xiu", (vol, run_cds)) return 2 return ret def _run_xius(self, vol: VFS, cds: list[int]): for cd in cds: self._run_xiu(vol, cd) self.xiu_busy = False self.xiu_asleep = True def _run_xiu(self, vol: VFS, cd: int): rp = vol.realpath cur = self.cur[rp] # t0 = time.time() with self.mutex: q = "select w,rd,fn from iu where c={} limit 80386" wrfs = cur.execute(q.format(cd)).fetchall() if not wrfs: return # dont wanna rebox so use format instead of prepared q = "delete from iu where w=? and +rd=? and +fn=? and +c={}" cur.executemany(q.format(cd), wrfs) cur.connection.commit() q = "select * from up where substr(w,1,16)=? and +rd=? and +fn=?" ups = [] for wrf in wrfs: up = cur.execute(q, wrf).fetchone() if up: ups.append(up) # t1 = time.time() # self.log("mapped {} warks in {:.3f} sec".format(len(wrfs), t1 - t0)) # "mapped 10989 warks in 0.126 sec" cmds = self.flags[rp]["xiu"] for cmd in cmds: m = self.xiu_ptn.search(cmd) ccd = int(m.group(1)) if m else 5 if ccd != cd: continue self.log("xiu: %d# %r" % (len(wrfs), cmd)) runihook(self.log, self.args.hook_v, cmd, vol, ups) def _vis_job_progress(self, job: dict[str, Any]) -> str: perc = 100 - (len(job["need"]) * 100.0 / (len(job["hash"]) or 1)) path = djoin(job["ptop"], job["prel"], job["name"]) return "{:5.1f}% {}".format(perc, path) def _vis_reg_progress(self, reg: dict[str, dict[str, Any]]) -> list[str]: ret = [] for _, job in reg.items(): if job["need"]: ret.append(self._vis_job_progress(job)) return ret def _expr_idx_filter(self, flags: dict[str, Any]) -> tuple[bool, dict[str, Any]]: if not self.no_expr_idx: return False, flags ret = {k: v for k, v in flags.items() if not k.startswith("e2t")} if len(ret) == len(flags): return False, flags return True, ret def init_indexes( self, all_vols: dict[str, VFS], scan_vols: list[str], fscan: bool, gid: int = 0 ) -> bool: if not gid: with self.mutex: gid = self.gid self.gt0 = time.time() nspin = 0 while True: nspin += 1 if nspin > 1: time.sleep(0.1) with self.mutex: if gid != self.gid: return False if self.pp: continue self.pp = ProgressPrinter(self.log, self.args) if not self.hub.is_dut: self.pp.start() break if gid: self.log("reload #%d running" % (gid,)) self.vfs = self.asrv.vfs self.acct = self.asrv.acct self.iacct = self.asrv.iacct self.grps = self.asrv.grps have_e2d = self.args.have_idp_hdrs or self.args.chpw or self.args.shr vols = list(all_vols.values()) t0 = time.time() if self.no_expr_idx: modified = False for vol in vols: m, f = self._expr_idx_filter(vol.flags) if m: vol.flags = f modified = True if modified: msg = "disabling -e2t because your sqlite belongs in a museum" self.log(msg, c=3) live_vols = [] with self.mutex, self.reg_mutex: # only need to protect register_vpath but all in one go feels right for vol in vols: if bos.path.isfile(vol.realpath): self.volstate[vol.vpath] = "online (just-a-file)" t = "NOTE: volume [/%s] is a file, not a folder" self.log(t % (vol.vpath,)) continue try: # mkdir gonna happen at snap anyways; bos.makedirs(vol.realpath, vf=vol.flags) dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath) except Exception as ex: self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" self.log("cannot access %s: %r" % (vol.realpath, ex), c=1) continue if scan_vols and vol.vpath not in scan_vols: continue if not self.register_vpath(vol.realpath, vol.flags): # self.log("db not enable for {}".format(m, vol.realpath)) continue live_vols.append(vol) if vol.vpath not in self.volstate: self.volstate[vol.vpath] = "OFFLINE (pending initialization)" vols = live_vols need_vac = {} need_mtag = False for vol in vols: if "e2t" in vol.flags: need_mtag = True if need_mtag and not self.mtag: self.mtag = MTag(self.log_func, self.args) if not self.mtag.usable: self.mtag = None # e2ds(a) volumes first if next((zv for zv in vols if "e2ds" in zv.flags), None): self._block("indexing") if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]: self.args.re_dhash = False with self.mutex, self.reg_mutex: self._drop_caches() for vol in vols: if self.stop or gid != self.gid: break en = set(vol.flags.get("mte", {})) self.entags[vol.realpath] = en if "e2d" in vol.flags: have_e2d = True if "e2ds" in vol.flags or fscan: self.volstate[vol.vpath] = "busy (hashing files)" _, vac = self._build_file_index(vol, list(all_vols.values())) if vac: need_vac[vol] = True if "e2v" in vol.flags: t = "online (integrity-check pending)" elif "e2ts" in vol.flags: t = "online (tags pending)" else: t = "online, idle" self.volstate[vol.vpath] = t self._unblock() # file contents verification for vol in vols: if self.stop: break if "e2v" not in vol.flags: continue t = "online (verifying integrity)" self.volstate[vol.vpath] = t self.log("{} [{}]".format(t, vol.realpath)) nmod = self._verify_integrity(vol) if nmod: self.log("modified {} entries in the db".format(nmod), 3) need_vac[vol] = True if "e2ts" in vol.flags: t = "online (tags pending)" else: t = "online, idle" self.volstate[vol.vpath] = t # open the rest + do any e2ts(a) needed_mutagen = False for vol in vols: if self.stop: break if "e2ts" not in vol.flags: continue t = "online (reading tags)" self.volstate[vol.vpath] = t self.log("{} [{}]".format(t, vol.realpath)) nadd, nrm, success = self._build_tags_index(vol) if not success: needed_mutagen = True if nadd or nrm: need_vac[vol] = True self.volstate[vol.vpath] = "online (mtp soon)" for vol in need_vac: with self.mutex, self.reg_mutex: reg = self.register_vpath(vol.realpath, vol.flags) assert reg # !rm cur, _ = reg with self.mutex: cur.connection.commit() cur.execute("vacuum") if self.stop: return False for vol in all_vols.values(): if vol.flags["dbd"] == "acid": continue with self.mutex, self.reg_mutex: reg = self.register_vpath(vol.realpath, vol.flags) try: assert reg # !rm cur, db_path = reg if bos.path.getsize(db_path + "-wal") < 1024 * 1024 * 5: continue except: continue try: with self.mutex: cur.execute("pragma wal_checkpoint(truncate)") try: cur.execute("commit") # absolutely necessary! for some reason except: pass cur.connection.commit() # this one maybe not except Exception as ex: self.log("checkpoint failed: {}".format(ex), 3) if self.stop: return False self.pp.end = True msg = "{} volumes in {:.2f} sec" self.log(msg.format(len(vols), time.time() - t0)) if needed_mutagen: msg = "could not read tags because no backends are available (Mutagen or FFprobe)" self.log(msg, c=1) t = "online (running mtp)" if self.mtag else "online, idle" for vol in vols: self.volstate[vol.vpath] = t if self.mtag: Daemon(self._run_all_mtp, "up2k-mtp-scan", (gid,)) else: self.unpp() return have_e2d def register_vpath( self, ptop: str, flags: dict[str, Any] ) -> Optional[tuple["sqlite3.Cursor", str]]: """mutex(main,reg) me""" histpath = self.vfs.dbpaths.get(ptop) if not histpath: self.log("no dbpath for %r" % (ptop,)) return None db_path = os.path.join(histpath, "up2k.db") if ptop in self.registry: try: return self.cur[ptop], db_path except: return None vpath = "?" for k, v in self.vfs.all_vols.items(): if v.realpath == ptop: vpath = k _, flags = self._expr_idx_filter(flags) n4g = bool(flags.get("noforget")) ft = "\033[0;32m{}{:.0}" ff = "\033[0;35m{}{:.0}" fv = "\033[0;36m{}:\033[90m{}" zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v" fx = set(zs.split()) fd = vf_bmap() fd.update(vf_cmap()) fd.update(vf_vmap()) fd = {v: k for k, v in fd.items()} fl = { k: v for k, v in flags.items() if k not in fd or ( v != getattr(self.args, fd[k]) and str(v) != str(getattr(self.args, fd[k])) ) } for k1, k2 in vf_cmap().items(): if k1 not in fl or k1 in fx: continue if str(fl[k1]) == str(getattr(self.args, k2)): del fl[k1] else: fl[k1] = ",".join(x for x in fl[k1]) if fl["chmod_d"] == int(self.args.chmod_d, 8): fl.pop("chmod_d") try: if fl["chmod_f"] == int(self.args.chmod_f or "-1", 8): fl.pop("chmod_f") except: pass for k in ("chmod_f", "chmod_d"): try: fl[k] = "%o" % (fl[k]) except: pass a = [ (ft if v is True else ff if v is False else fv).format(k, str(v)) for k, v in fl.items() if k not in fx ] if not a: a = ["\033[90mall-default"] if a: zs = " ".join(sorted(a)) zs = zs.replace("90mre.compile(", "90m(") # nohash self.log("/{} {}".format(vpath + ("/" if vpath else ""), zs), "35") reg = {} drp = None emptylist = [] dotpart = "." if self.args.dotpart else "" snap = os.path.join(histpath, "up2k.snap") if bos.path.exists(snap): with gzip.GzipFile(snap, "rb") as f: j = f.read().decode("utf-8") reg2 = json.loads(j) try: drp = reg2["droppable"] reg2 = reg2["registry"] except: pass reg = reg2 # diff-golf if reg2 and "dwrk" not in reg2[next(iter(reg2))]: for job in reg2.values(): job["dwrk"] = job["wark"] rm = [] for k, job in reg2.items(): job["ptop"] = ptop is_done = "done" in job if is_done: job["need"] = job["hash"] = emptylist else: if "need" not in job: job["need"] = [] if "hash" not in job: job["hash"] = [] if is_done: fp = djoin(ptop, job["prel"], job["name"]) else: fp = djoin(ptop, job["prel"], dotpart + job["name"] + ".PARTIAL") if bos.path.exists(fp): if is_done: continue job["poke"] = time.time() job["busy"] = {} else: self.log("ign deleted file in snap: %r" % (fp,)) if not n4g: rm.append(k) for x in rm: del reg2[x] # optimize pre-1.15.4 entries if next((x for x in reg.values() if "done" in x and "poke" in x), None): zsl = "host tnam busy sprs poke t0c".split() for job in reg.values(): if "done" in job: for k in zsl: job.pop(k, None) if drp is None: drp = [k for k, v in reg.items() if not v["need"]] else: drp = [x for x in drp if x in reg] t = "loaded snap {} |{}| ({})".format(snap, len(reg.keys()), len(drp or [])) ta = [t] + self._vis_reg_progress(reg) self.log("\n".join(ta)) self.flags[ptop] = flags self.vol_act[ptop] = 0.0 self.registry[ptop] = reg self.droppable[ptop] = drp or [] self.regdrop(ptop, "") if not HAVE_SQLITE3 or "e2d" not in flags or "d2d" in flags: return None try: if bos.makedirs(histpath): hidedir(histpath) except Exception as ex: t = "failed to initialize volume '/%s': %s" self.log(t % (vpath, ex), 1) return None try: cur = self._open_db_wd(db_path) # speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb) dbd = flags["dbd"] if dbd == "acid": # 217.5s; python-defaults zs = "delete" sync = "full" elif dbd == "swal": # 88.0s; still 99.9% safe (can lose a bit of on OS crash) zs = "wal" sync = "full" elif dbd == "yolo": # 2.7s; may lose entire db on OS crash zs = "wal" sync = "off" else: # 4.1s; corruption-safe but more likely to lose wal zs = "wal" sync = "normal" try: amode = cur.execute("pragma journal_mode=" + zs).fetchone()[0] if amode.lower() != zs.lower(): t = "sqlite failed to set journal_mode {}; got {}" raise Exception(t.format(zs, amode)) except Exception as ex: if sync != "off": sync = "full" t = "reverting to sync={} because {}" self.log(t.format(sync, ex)) cur.execute("pragma synchronous=" + sync) cur.connection.commit() self._verify_db_cache(cur, vpath) self.cur[ptop] = cur self.volsize[cur] = 0 self.volnfiles[cur] = 0 return cur, db_path except: msg = "ERROR: cannot use database at [%s]:\n%s\n\033[33mhint: %s\n" self.log(msg % (db_path, traceback.format_exc(), HINT_HISTPATH), 1) return None def _verify_db_cache(self, cur: "sqlite3.Cursor", vpath: str) -> None: # check if list of intersecting volumes changed since last use; drop caches if so prefix = (vpath + "/").lstrip("/") vps = [x for x in self.vfs.all_vols if x.startswith(prefix)] vps.sort() seed = [x[len(prefix) :] for x in vps] # also consider volflags which affect indexing for vp in vps: vf = self.vfs.all_vols[vp].flags vf = {k: v for k, v in vf.items() if k in VF_AFFECTS_INDEXING} seed.append(str(sorted(vf.items()))) zb = hashlib.sha1("\n".join(seed).encode("utf-8", "replace")).digest() vcfg = ub64enc(zb[:18]).decode("ascii") c = cur.execute("select v from kv where k = 'volcfg'") try: (oldcfg,) = c.fetchone() except: oldcfg = "" if oldcfg != vcfg: cur.execute("delete from kv where k = 'volcfg'") cur.execute("delete from dh") cur.execute("delete from cv") cur.execute("insert into kv values ('volcfg',?)", (vcfg,)) cur.connection.commit() def _build_file_index(self, vol: VFS, all_vols: list[VFS]) -> tuple[bool, bool]: do_vac = False top = vol.realpath rei = vol.flags.get("noidx") reh = vol.flags.get("nohash") n4g = bool(vol.flags.get("noforget")) ffat = "fat32" in vol.flags cst = bos.stat(top) dev = cst.st_dev if vol.flags.get("xdev") else 0 with self.mutex: with self.reg_mutex: reg = self.register_vpath(top, vol.flags) assert reg and self.pp # !rm cur, db_path = reg db = Dbw(cur, 0, 0, time.time()) self.pp.n = next(db.c.execute("select count(w) from up"))[0] excl = [ vol.realpath + "/" + d.vpath[len(vol.vpath) :].lstrip("/") for d in all_vols if d != vol and (d.vpath.startswith(vol.vpath + "/") or not vol.vpath) ] excl += [absreal(x) for x in excl] excl += list(self.vfs.histtab.values()) excl += list(self.vfs.dbpaths.values()) if WINDOWS: excl = [x.replace("/", "\\") for x in excl] else: # ~/.wine/dosdevices/z:/ and such excl.extend(("/dev", "/proc", "/run", "/sys")) excl = list({k: 1 for k in excl}) if self.args.re_dirsz: db.c.execute("delete from ds") db.n += 1 rtop = absreal(top) n_add = n_rm = 0 try: if dir_is_empty(self.log_func, not self.args.no_scandir, rtop): t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem" self.log(t % (vol.vpath, rtop), 6) return True, False if not vol.check_landmarks(): t = "volume /%s at [%s] will not be indexed due to bad landmarks" self.log(t % (vol.vpath, rtop), 6) return True, False n_add, _, _ = self._build_dir( db, top, set(excl), top, rtop, rei, reh, n4g, ffat, [], cst, dev, bool(vol.flags.get("xvol")), ) if not n4g: n_rm = self._drop_lost(db.c, top, excl) except Exception as ex: t = "failed to index volume [{}]:\n{}" self.log(t.format(top, min_ex()), c=1) if db_ex_chk(self.log, ex, db_path): self.hub.log_stacks() if db.n: self.log("commit %d new files; %d updates" % (db.nf, db.n)) if self.args.no_dhash: if db.c.execute("select d from dh").fetchone(): db.c.execute("delete from dh") self.log("forgetting dhashes in {}".format(top)) elif n_add or n_rm: self._set_tagscan(db.c, True) db.c.connection.commit() if ( vol.flags.get("vmaxb") or vol.flags.get("vmaxn") or (self.args.stats and not self.args.nos_vol) ): zs = "select count(sz), sum(sz) from up" vn, vb = db.c.execute(zs).fetchone() vb = vb or 0 vb += vn * 2048 self.volsize[db.c] = vb self.volnfiles[db.c] = vn vmaxb = unhumanize(vol.flags.get("vmaxb") or "0") vmaxn = unhumanize(vol.flags.get("vmaxn") or "0") t = "{:>5} / {:>5} ( {:>5} / {:>5} files) in {}".format( humansize(vb, True), humansize(vmaxb, True), humansize(vn, True).rstrip("B"), humansize(vmaxn, True).rstrip("B"), vol.realpath, ) self.log(t) return True, bool(n_add or n_rm or do_vac) def _fika(self, db: Dbw) -> None: zs = self.fika self.fika = "" if zs not in self.args.fika: return t = "fika(%s); commit %d new files; %d updates" self.log(t % (zs, db.nf, db.n)) db.c.connection.commit() db.n = db.nf = 0 db.t = time.time() self.mutex.release() time.sleep(0.5) self.mutex.acquire() db.t = time.time() def _build_dir( self, db: Dbw, top: str, excl: set[str], cdir: str, rcdir: str, rei: Optional[Pattern[str]], reh: Optional[Pattern[str]], n4g: bool, ffat: bool, seen: list[str], cst: os.stat_result, dev: int, xvol: bool, ) -> tuple[int, int, int]: if xvol and not rcdir.startswith(top): self.log("skip xvol: %r -> %r" % (cdir, rcdir), 6) return 0, 0, 0 if rcdir in seen: t = "bailing from symlink loop,\n prev: %r\n curr: %r\n from: %r" self.log(t % (seen[-1], rcdir, cdir), 3) return 0, 0, 0 # total-files-added, total-num-files, recursive-size tfa = tnf = rsz = 0 seen = seen + [rcdir] unreg: list[str] = [] files: list[tuple[int, int, str]] = [] fat32 = True cv = vcv = acv = "" e_d = {} th_cvd = self.args.th_coversd th_cvds = self.args.th_coversd_set scan_pr_s = self.args.scan_pr_s assert self.pp and self.mem_cur # !rm self.pp.msg = "a%d %s" % (self.pp.n, cdir) rd = cdir[len(top) :].strip("/") if WINDOWS: rd = rd.replace("\\", "/").strip("/") rds = rd + "/" if rd else "" cdirs = cdir + os.sep g = statdir(self.log_func, not self.args.no_scandir, True, cdir, False) gl = sorted(g) partials = set([x[0] for x in gl if "PARTIAL" in x[0]]) for iname, inf in gl: if self.fika: if not self.stop: self._fika(db) if self.stop: self.fika = "f" return -1, 0, 0 rp = rds + iname abspath = cdirs + iname if rei and rei.search(abspath): unreg.append(rp) continue lmod = int(inf.st_mtime) if stat.S_ISLNK(inf.st_mode): try: inf = bos.stat(abspath) except: continue sz = inf.st_size if fat32 and not ffat and inf.st_mtime % 2: fat32 = False if stat.S_ISDIR(inf.st_mode): rap = absreal(abspath) if ( dev and inf.st_dev != dev and not (ANYWIN and bos.stat(rap).st_dev == dev) ): self.log("skip xdev %s->%s: %r" % (dev, inf.st_dev, abspath), 6) continue if abspath in excl or rap in excl: unreg.append(rp) continue if iname == ".th" and bos.path.isdir(os.path.join(abspath, "top")): # abandoned or foreign, skip continue # self.log(" dir: {}".format(abspath)) try: i1, i2, i3 = self._build_dir( db, top, excl, abspath, rap, rei, reh, n4g, fat32, seen, inf, dev, xvol, ) tfa += i1 tnf += i2 rsz += i3 except: t = "failed to index subdir %r:\n%s" self.log(t % (abspath, min_ex()), 1) elif not stat.S_ISREG(inf.st_mode): self.log("skip type-0%o file %r" % (inf.st_mode, abspath)) else: # self.log("file: {}".format(abspath)) if rp.endswith(".PARTIAL") and time.time() - lmod < 60: # rescan during upload continue if not sz and ( "%s.PARTIAL" % (iname,) in partials or ".%s.PARTIAL" % (iname,) in partials ): # placeholder for unfinished upload continue rsz += sz files.append((sz, lmod, iname)) if sz: liname = iname.lower() ext = liname.rsplit(".", 1)[-1] if ( liname in th_cvds or (not cv and ext in ICV_EXTS and not iname.startswith(".")) ) and ( not cv or liname not in th_cvds or cv.lower() not in th_cvds or th_cvd.index(liname) < th_cvd.index(cv.lower()) ): cv = iname elif not vcv and ext in VCV_EXTS and not iname.startswith("."): vcv = iname elif not acv and ext in ACV_EXTS and not iname.startswith("."): acv = iname if not cv: cv = vcv or acv if not self.args.no_dirsz: tnf += len(files) q = "select sz, nf from ds where rd=? limit 1" try: db_sz, db_nf = db.c.execute(q, (rd,)).fetchone() or (-1, -1) if rsz != db_sz or tnf != db_nf: db.c.execute("delete from ds where rd=?", (rd,)) db.c.execute("insert into ds values (?,?,?)", (rd, rsz, tnf)) db.n += 1 except: pass # mojibake rd # folder of 1000 files = ~1 MiB RAM best-case (tiny filenames); # free up stuff we're done with before dhashing gl = [] partials.clear() if not self.args.no_dhash: if len(files) < 9000: zh = hashlib.sha1(str(files).encode("utf-8", "replace")) else: zh = hashlib.sha1() _ = [zh.update(str(x).encode("utf-8", "replace")) for x in files] zh.update(cv.encode("utf-8", "replace")) zh.update(spack(b" 1: t = "WARN: multiple entries: %r => %r |%d|\n%r" rep_db = "\n".join([repr(x) for x in in_db]) self.log(t % (top, rp, len(in_db), rep_db)) dts = -1 if fat32 and abs(dts - lmod) == 1: dts = lmod if dts == lmod and dsz == sz and (nohash or dw[0] != "#" or not sz): continue if un is None: un = "" t = "reindex %r => %r mtime(%s/%s) size(%s/%s)" self.log(t % (top, rp, dts, lmod, dsz, sz)) self.db_rm(db.c, e_d, rd, fn, 0) tfa += 1 db.n += 1 in_db = [] else: dw = "" ip = "" at = 0 un = "" self.pp.msg = "a%d %s" % (self.pp.n, abspath) if nohash or not sz: wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn) else: if sz > 1024 * 1024 * scan_pr_s: self.log("file: %r" % (abspath,)) try: hashes, _ = self._hashlist_from_file( abspath, "a{}, ".format(self.pp.n) ) except Exception as ex: self._ex_hash(ex, abspath) continue if not hashes: return -1, 0, 0 wark = up2k_wark_from_hashlist(self.salt, sz, hashes) if dw and dw != wark: ip = "" at = 0 un = "" # skip upload hooks by not providing vflags self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at) db.n += 1 db.nf += 1 tfa += 1 td = time.time() - db.t if db.n >= 4096 or td >= 60: self.log("commit %d new files; %d updates" % (db.nf, db.n)) db.c.connection.commit() db.n = db.nf = 0 db.t = time.time() if not self.args.no_dhash: db.c.execute("delete from dh where d = ?", (drd,)) # type: ignore db.c.execute("insert into dh values (?,?)", (drd, dhash)) # type: ignore if self.stop: return -1, 0, 0 # drop shadowed folders for sh_rd in unreg: n = 0 q = "select count(w) from up where (rd=? or rd like ?||'/%')" for sh_erd in [sh_rd, "//" + w8b64enc(sh_rd)]: try: erd_erd = (sh_erd, sh_erd) n = db.c.execute(q, erd_erd).fetchone()[0] break except: pass assert erd_erd # type: ignore # !rm if n: t = "forgetting %d shadowed autoindexed files in %r > %r" self.log(t % (n, top, sh_rd)) q = "delete from dh where (d = ? or d like ?||'/%')" db.c.execute(q, erd_erd) q = "delete from up where (rd=? or rd like ?||'/%')" db.c.execute(q, erd_erd) tfa += n q = "delete from ds where (rd=? or rd like ?||'/%')" db.c.execute(q, erd_erd) if n4g: return tfa, tnf, rsz # drop missing files q = "select fn from up where rd = ?" try: c = db.c.execute(q, (rd,)) except: c = db.c.execute(q, ("//" + w8b64enc(rd),)) hits = [w8b64dec(x[2:]) if x.startswith("//") else x for (x,) in c] rm_files = [x for x in hits if x not in seen_files] n_rm = len(rm_files) for fn in rm_files: self.db_rm(db.c, e_d, rd, fn, 0) if n_rm: self.log("forgot {} deleted files".format(n_rm)) return tfa, tnf, rsz def _drop_lost(self, cur: "sqlite3.Cursor", top: str, excl: list[str]) -> int: rm = [] n_rm = 0 nchecked = 0 assert self.pp # `_build_dir` did all unshadowed files; first do dirs: ndirs = next(cur.execute("select count(distinct rd) from up"))[0] c = cur.execute("select distinct rd from up order by rd desc") for (drd,) in c: nchecked += 1 if drd.startswith("//"): rd = w8b64dec(drd[2:]) else: rd = drd abspath = djoin(top, rd) self.pp.msg = "b%d %s" % (ndirs - nchecked, abspath) try: if os.path.isdir(abspath): continue except: pass rm.append(drd) if rm: q = "select count(w) from up where rd = ?" for rd in rm: n_rm += next(cur.execute(q, (rd,)))[0] self.log("forgetting {} deleted dirs, {} files".format(len(rm), n_rm)) for rd in rm: cur.execute("delete from dh where d = ?", (rd,)) cur.execute("delete from up where rd = ?", (rd,)) # then shadowed deleted files n_rm2 = 0 c2 = cur.connection.cursor() excl = [x[len(top) + 1 :] for x in excl if x.startswith(top + "/")] q = "select rd, fn from up where (rd = ? or rd like ?||'%') order by rd" for rd in excl: for erd in [rd, "//" + w8b64enc(rd)]: try: c = cur.execute(q, (erd, erd + "/")) break except: pass crd = "///" cdc: set[str] = set() for drd, dfn in c: rd, fn = s3dec(drd, dfn) if crd != rd: crd = rd try: cdc = set(os.listdir(djoin(top, rd))) except: cdc.clear() if fn not in cdc: q = "delete from up where rd = ? and fn = ?" c2.execute(q, (drd, dfn)) n_rm2 += 1 if n_rm2: self.log("forgetting {} shadowed deleted files".format(n_rm2)) c2.connection.commit() # then covers n_rm3 = 0 qu = "select 1 from up where rd=? and fn=? limit 1" q = "delete from cv where rd=? and dn=? and +fn=?" for crd, cdn, fn in cur.execute("select * from cv"): urd = vjoin(crd, cdn) if not c2.execute(qu, (urd, fn)).fetchone(): c2.execute(q, (crd, cdn, fn)) n_rm3 += 1 if n_rm3: self.log("forgetting {} deleted covers".format(n_rm3)) c2.connection.commit() c2.close() return n_rm + n_rm2 def _verify_integrity(self, vol: VFS) -> int: """expensive; blocks database access until finished""" ptop = vol.realpath assert self.pp cur = self.cur[ptop] rei = vol.flags.get("noidx") reh = vol.flags.get("nohash") e2vu = "e2vu" in vol.flags e2vp = "e2vp" in vol.flags excl = [ d[len(vol.vpath) :].lstrip("/") for d in self.vfs.all_vols if d != vol.vpath and (d.startswith(vol.vpath + "/") or not vol.vpath) ] qexa: list[str] = [] pexa: list[str] = [] for vpath in excl: qexa.append("up.rd != ? and not up.rd like ?||'%'") pexa.extend([vpath, vpath]) pex: tuple[Any, ...] = tuple(pexa) qex = " and ".join(qexa) if qex: qex = " where " + qex rewark: list[tuple[str, str, str, int, int]] = [] f404: list[tuple[str, str, str]] = [] with self.mutex: b_left = 0 n_left = 0 q = "select sz from up" + qex for (sz,) in cur.execute(q, pex): b_left += sz # sum() can overflow according to docs n_left += 1 tf, _ = self._spool_warks(cur, "select w, rd, fn from up" + qex, pex, 0) with gzip.GzipFile(mode="rb", fileobj=tf) as gf: for zb in gf: if self.stop: return -1 zs = zb[:-1].decode("utf-8").replace("\x00\x02", "\n") w, drd, dfn = zs.split("\x00\x01") with self.mutex: q = "select mt, sz from up where rd=? and fn=? and +w=?" try: mt, sz = cur.execute(q, (drd, dfn, w)).fetchone() except: # file moved/deleted since spooling continue n_left -= 1 b_left -= sz if drd.startswith("//") or dfn.startswith("//"): rd, fn = s3dec(drd, dfn) else: rd = drd fn = dfn abspath = djoin(ptop, rd, fn) if rei and rei.search(abspath): continue nohash = reh.search(abspath) if reh else False pf = "v{}, {:.0f}+".format(n_left, b_left / 1024 / 1024) self.pp.msg = pf + abspath try: stl = bos.lstat(abspath) st = bos.stat(abspath) if stat.S_ISLNK(stl.st_mode) else stl except Exception as ex: self.log("missing file: %r" % (abspath,), 3) f404.append((drd, dfn, w)) continue mt2 = int(stl.st_mtime) sz2 = st.st_size if nohash or not sz2: w2 = up2k_wark_from_metadata(self.salt, sz2, mt2, rd, fn) else: if sz2 > 1024 * 1024 * 32: self.log("file: %r" % (abspath,)) try: hashes, _ = self._hashlist_from_file(abspath, pf) except Exception as ex: self._ex_hash(ex, abspath) continue if not hashes: return -1 w2 = up2k_wark_from_hashlist(self.salt, sz2, hashes) if w == w2: continue # symlink mtime was inconsistent before v1.9.4; check if that's it if st != stl and (nohash or not sz2): mt2b = int(st.st_mtime) w2b = up2k_wark_from_metadata(self.salt, sz2, mt2b, rd, fn) if w == w2b: continue rewark.append((drd, dfn, w2, sz2, mt2)) t = "hash mismatch: %r\n db: %s (%d byte, %d)\n fs: %s (%d byte, %d)" self.log(t % (abspath, w, sz, mt, w2, sz2, mt2), 1) if e2vp and (rewark or f404): self.hub.retcode = 1 Daemon(self.hub.sigterm) t = "in volume /%s: %s files missing, %s files have incorrect hashes" t = t % (vol.vpath, len(f404), len(rewark)) self.log(t, 1) raise Exception(t) if not e2vu or (not rewark and not f404): return 0 with self.mutex: q = "update up set w=?, sz=?, mt=? where rd=? and fn=?" for rd, fn, w, sz, mt in rewark: cur.execute(q, (w, sz, int(mt), rd, fn)) if f404: q = "delete from up where rd=? and fn=? and +w=?" cur.executemany(q, f404) cur.connection.commit() return len(rewark) + len(f404) def _build_tags_index(self, vol: VFS) -> tuple[int, int, bool]: ptop = vol.realpath with self.mutex, self.reg_mutex: reg = self.register_vpath(ptop, vol.flags) assert reg and self.pp cur = self.cur[ptop] if not self.args.no_dhash: with self.mutex: c = cur.execute("select k from kv where k = 'tagscan'") if not c.fetchone(): return 0, 0, bool(self.mtag) ret = self._build_tags_index_2(ptop) with self.mutex: self._set_tagscan(cur, False) cur.connection.commit() return ret def _drop_caches(self) -> None: """mutex(main,reg) me""" self.log("dropping caches for a full filesystem scan") for vol in self.vfs.all_vols.values(): reg = self.register_vpath(vol.realpath, vol.flags) if not reg: continue cur, _ = reg self._set_tagscan(cur, True) cur.execute("delete from dh") cur.execute("delete from cv") cur.connection.commit() def _set_tagscan(self, cur: "sqlite3.Cursor", need: bool) -> bool: if self.args.no_dhash: return False c = cur.execute("select k from kv where k = 'tagscan'") if bool(c.fetchone()) == need: return False if need: cur.execute("insert into kv values ('tagscan',1)") else: cur.execute("delete from kv where k = 'tagscan'") return True def _build_tags_index_2(self, ptop: str) -> tuple[int, int, bool]: entags = self.entags[ptop] flags = self.flags[ptop] cur = self.cur[ptop] n_add = 0 n_rm = 0 if "e2tsr" in flags: with self.mutex: n_rm = cur.execute("select count(w) from mt").fetchone()[0] if n_rm: self.log("discarding {} media tags for a full rescan".format(n_rm)) cur.execute("delete from mt") # integrity: drop tags for tracks that were deleted if "e2t" in flags: with self.mutex: n = 0 c2 = cur.connection.cursor() up_q = "select w from up where substr(w,1,16) = ?" rm_q = "delete from mt where w = ?" for (w,) in cur.execute("select w from mt"): if not c2.execute(up_q, (w,)).fetchone(): c2.execute(rm_q, (w[:16],)) n += 1 c2.close() if n: t = "discarded media tags for {} deleted files" self.log(t.format(n)) n_rm += n with self.mutex: cur.connection.commit() # bail if a volflag disables indexing if "d2t" in flags or "d2d" in flags: return 0, n_rm, True # add tags for new files if "e2ts" in flags: if not self.mtag: return 0, n_rm, False nq = 0 with self.mutex: tf, nq = self._spool_warks( cur, "select w from up order by rd, fn", (), 1 ) if not nq: # self.log("tags ok") self._unspool(tf) return 0, n_rm, True if nq == -1: return -1, -1, True with gzip.GzipFile(mode="rb", fileobj=tf) as gf: n_add = self._e2ts_q(gf, nq, cur, ptop, entags) self._unspool(tf) return n_add, n_rm, True def _e2ts_q( self, qf: gzip.GzipFile, nq: int, cur: "sqlite3.Cursor", ptop: str, entags: set[str], ) -> int: assert self.pp and self.mtag flags = self.flags[ptop] mpool: Optional[Queue[Mpqe]] = None if self.mtag.prefer_mt and self.args.mtag_mt > 1: mpool = self._start_mpool() n_add = 0 n_buf = 0 last_write = time.time() for bw in qf: if self.stop: return -1 w = bw[:-1].decode("ascii") w16 = w[:16] with self.mutex: try: q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? and +w=?" rd, fn, ip, at, un = cur.execute(q, (w16, w)).fetchone() except: # file modified/deleted since spooling continue if rd.startswith("//") or fn.startswith("//"): rd, fn = s3dec(rd, fn) if "mtp" in flags: q = "select 1 from mt where w=? and +k='t:mtp' limit 1" if cur.execute(q, (w16,)).fetchone(): continue q = "insert into mt values (?,'t:mtp','a')" cur.execute(q, (w16,)) abspath = djoin(ptop, rd, fn) self.pp.msg = "c%d %s" % (nq, abspath) if not mpool: n_tags = self._tagscan_file(cur, entags, flags, w, abspath, ip, at, un) else: oth_tags = {} if ip: oth_tags["up_ip"] = ip if at: oth_tags["up_at"] = at if un: oth_tags["up_by"] = un mpool.put(Mpqe({}, entags, flags, w, abspath, oth_tags)) with self.mutex: n_tags = len(self._flush_mpool(cur)) n_add += n_tags n_buf += n_tags nq -= 1 td = time.time() - last_write if n_buf >= 4096 or td >= self.timeout / 2: self.log("commit {} new tags".format(n_buf)) with self.mutex: cur.connection.commit() last_write = time.time() n_buf = 0 if mpool: self._stop_mpool(mpool) with self.mutex: n_add += len(self._flush_mpool(cur)) with self.mutex: cur.connection.commit() return n_add def _spool_warks( self, cur: "sqlite3.Cursor", q: str, params: tuple[Any, ...], flt: int, ) -> tuple[tempfile.SpooledTemporaryFile[bytes], int]: """mutex(main) me""" n = 0 c2 = cur.connection.cursor() tf = tempfile.SpooledTemporaryFile(1024 * 1024 * 8, "w+b", prefix="cpp-tq-") with gzip.GzipFile(mode="wb", fileobj=tf) as gf: for row in cur.execute(q, params): if self.stop: return tf, -1 if flt == 1: q = "select 1 from mt where w=? and +k != 't:mtp'" if c2.execute(q, (row[0][:16],)).fetchone(): continue zs = "\x00\x01".join(row).replace("\n", "\x00\x02") gf.write((zs + "\n").encode("utf-8")) n += 1 c2.close() tf.seek(0) self.spools.add(tf) return tf, n def _unspool(self, tf: tempfile.SpooledTemporaryFile[bytes]) -> None: try: self.spools.remove(tf) except: return try: tf.close() except Exception as ex: self.log("failed to delete spool: {}".format(ex), 3) def _flush_mpool(self, wcur: "sqlite3.Cursor") -> list[str]: ret = [] for x in self.pending_tags: self._tag_file(wcur, *x) ret.append(x[1]) self.pending_tags = [] return ret def _run_all_mtp(self, gid: int) -> None: t0 = time.time() for ptop, flags in self.flags.items(): if "mtp" in flags: if ptop not in self.entags: t = "skipping mtp for unavailable volume {}" self.log(t.format(ptop), 1) else: self._run_one_mtp(ptop, gid) vtop = "\n" for vol in self.vfs.all_vols.values(): if vol.realpath == ptop: vtop = vol.vpath if "running mtp" in self.volstate.get(vtop, ""): self.volstate[vtop] = "online, idle" td = time.time() - t0 msg = "mtp finished in {:.2f} sec ({})" self.log(msg.format(td, s2hms(td, True))) self.unpp() if self.args.exit == "idx": self.hub.sigterm() def _run_one_mtp(self, ptop: str, gid: int) -> None: if gid != self.gid: return entags = self.entags[ptop] vf = self.flags[ptop] parsers = {} for parser in vf["mtp"]: try: parser = MParser(parser) except: self.log("invalid argument (could not find program): " + parser, 1) return for tag in entags: if tag in parser.tags: parsers[parser.tag] = parser if self.args.mtag_vv: t = "parsers for {}: \033[0m{}" self.log(t.format(ptop, list(parsers.keys())), "90") self.mtp_parsers[ptop] = parsers q = "select count(w) from mt where k = 't:mtp'" with self.mutex: cur = self.cur[ptop] cur = cur.connection.cursor() wcur = cur.connection.cursor() n_left = cur.execute(q).fetchone()[0] mpool = self._start_mpool() batch_sz = mpool.maxsize * 3 t_prev = time.time() n_prev = n_left n_done = 0 to_delete = {} in_progress = {} while True: did_nothing = True with self.mutex: if gid != self.gid: break q = "select w from mt where k = 't:mtp' limit ?" zq = cur.execute(q, (batch_sz,)).fetchall() warks = [str(x[0]) for x in zq] jobs = [] for w in warks: if w in in_progress: continue q = "select rd, fn, ip, at, un from up where substr(w,1,16)=? limit 1" rd, fn, ip, at, un = cur.execute(q, (w,)).fetchone() rd, fn = s3dec(rd, fn) abspath = djoin(ptop, rd, fn) q = "select k from mt where w = ?" zq = cur.execute(q, (w,)).fetchall() have: dict[str, Union[str, float]] = {x[0]: 1 for x in zq} did_nothing = False parsers = self._get_parsers(ptop, have, abspath) if not parsers: to_delete[w] = True n_left -= 1 continue if next((x for x in parsers.values() if x.pri), None): q = "select k, v from mt where w = ?" zq2 = cur.execute(q, (w,)).fetchall() oth_tags = {str(k): v for k, v in zq2} else: oth_tags = {} if ip: oth_tags["up_ip"] = ip if at: oth_tags["up_at"] = at if un: oth_tags["up_by"] = un jobs.append(Mpqe(parsers, set(), vf, w, abspath, oth_tags)) in_progress[w] = True with self.mutex: done = self._flush_mpool(wcur) for w in done: to_delete[w] = True did_nothing = False in_progress.pop(w) n_done += 1 for w in to_delete: q = "delete from mt where w = ? and +k = 't:mtp'" cur.execute(q, (w,)) to_delete = {} if not warks: break if did_nothing: with self.tag_event: self.tag_event.wait(0.2) if not jobs: continue try: now = time.time() s = ((now - t_prev) / (n_prev - n_left)) * n_left h, s = divmod(s, 3600) m, s = divmod(s, 60) n_prev = n_left t_prev = now except: h = 1 m = 1 msg = "mtp: {} done, {} left, eta {}h {:02d}m" with self.mutex: msg = msg.format(n_done, n_left, int(h), int(m)) self.log(msg, c=6) for j in jobs: n_left -= 1 mpool.put(j) with self.mutex: cur.connection.commit() self._stop_mpool(mpool) with self.mutex: done = self._flush_mpool(wcur) for w in done: q = "delete from mt where w = ? and +k = 't:mtp'" cur.execute(q, (w,)) cur.connection.commit() if n_done: self.log("mtp: scanned {} files in {}".format(n_done, ptop), c=6) cur.execute("vacuum") wcur.close() cur.close() def _get_parsers( self, ptop: str, have: dict[str, Union[str, float]], abspath: str ) -> dict[str, MParser]: try: all_parsers = self.mtp_parsers[ptop] except: if self.args.mtag_vv: self.log("no mtp defined for {}".format(ptop), "90") return {} entags = self.entags[ptop] parsers = {} for k, v in all_parsers.items(): if "ac" in entags or ".aq" in entags: if "ac" in have or ".aq" in have: # is audio, require non-audio? if v.audio == "n": if self.args.mtag_vv: t = "skip mtp {}; want no-audio, got audio" self.log(t.format(k), "90") continue # is not audio, require audio? elif v.audio == "y": if self.args.mtag_vv: t = "skip mtp {}; want audio, got no-audio" self.log(t.format(k), "90") continue if v.ext: match = False for ext in v.ext: if abspath.lower().endswith("." + ext): match = True break if not match: if self.args.mtag_vv: t = "skip mtp {}; want file-ext {}, got {}" self.log(t.format(k, v.ext, abspath.rsplit(".")[-1]), "90") continue parsers[k] = v parsers = {k: v for k, v in parsers.items() if v.force or k not in have} return parsers def _start_mpool(self) -> Queue[Mpqe]: # mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor # both do crazy runahead so lets reinvent another wheel nw = max(1, self.args.mtag_mt) assert self.mtag # !rm if not self.mpool_used: self.mpool_used = True self.log("using {}x {}".format(nw, self.mtag.backend)) mpool: Queue[Mpqe] = Queue(nw) for _ in range(nw): Daemon(self._tag_thr, "up2k-mpool", (mpool,)) return mpool def _stop_mpool(self, mpool: Queue[Mpqe]) -> None: if not mpool: return for _ in range(mpool.maxsize): mpool.put(Mpqe({}, set(), {}, "", "", {})) mpool.join() def _tag_thr(self, q: Queue[Mpqe]) -> None: assert self.mtag while True: qe = q.get() if not qe.w: q.task_done() return try: st = bos.stat(qe.abspath) if not qe.mtp: if self.args.mtag_vv: t = "tag-thr: {}({})" self.log(t.format(self.mtag.backend, qe.abspath), "90") tags = self.mtag.get(qe.abspath, qe.vf) if st.st_size else {} else: if self.args.mtag_vv: t = "tag-thr: {}({})" self.log(t.format(list(qe.mtp.keys()), qe.abspath), "90") tags = self.mtag.get_bin(qe.mtp, qe.abspath, qe.oth_tags) vtags = [ "\033[36m{} \033[33m{}".format(k, v) for k, v in tags.items() ] if vtags: self.log("{}\033[0m [{}]".format(" ".join(vtags), qe.abspath)) with self.mutex: self.pending_tags.append((qe.entags, qe.w, qe.abspath, tags)) except: ex = traceback.format_exc() self._log_tag_err(qe.mtp or self.mtag.backend, qe.abspath, ex) finally: if qe.mtp: with self.tag_event: self.tag_event.notify_all() q.task_done() def _log_tag_err(self, parser: Any, abspath: str, ex: Any) -> None: msg = "%s failed to read tags from %r:\n%s" % (parser, abspath, ex) self.log(msg.lstrip(), c=1 if " int: """will mutex(main)""" assert self.mtag # !rm try: st = bos.stat(abspath) except: return 0 if not stat.S_ISREG(st.st_mode): return 0 try: tags = self.mtag.get(abspath, vf) if st.st_size else {} except Exception as ex: self._log_tag_err("", abspath, ex) return 0 if ip: tags["up_ip"] = ip if at: tags["up_at"] = at if un: tags["up_by"] = un with self.mutex: return self._tag_file(write_cur, entags, wark, abspath, tags) def _tag_file( self, write_cur: "sqlite3.Cursor", entags: set[str], wark: str, abspath: str, tags: dict[str, Union[str, float]], ) -> int: """mutex(main) me""" assert self.mtag # !rm if not bos.path.isfile(abspath): return 0 if entags: tags = {k: v for k, v in tags.items() if k in entags} if not tags: # indicate scanned without tags tags = {"x": 0} if not tags: return 0 for k in tags.keys(): q = "delete from mt where w = ? and ({})".format( " or ".join(["+k = ?"] * len(tags)) ) args = [wark[:16]] + list(tags.keys()) write_cur.execute(q, tuple(args)) ret = 0 for k, v in tags.items(): q = "insert into mt values (?,?,?)" write_cur.execute(q, (wark[:16], k, v)) ret += 1 self._set_tagscan(write_cur, True) return ret def _trace(self, msg: str) -> None: self.log("ST: {}".format(msg)) def _open_db_wd(self, db_path: str) -> "sqlite3.Cursor": ok: list[int] = [] if not self.hub.is_dut: Daemon(self._open_db_timeout, "opendb_watchdog", [db_path, ok]) try: return self._open_db(db_path) finally: ok.append(1) def _open_db_timeout(self, db_path, ok: list[int]) -> None: # give it plenty of time due to the count statement (and wisdom from byte's box) for _ in range(60): time.sleep(1) if ok: return t = "WARNING:\n\n initializing an up2k database is taking longer than one minute; something has probably gone wrong:\n\n" self._log_sqlite_incompat(db_path, t) def _log_sqlite_incompat(self, db_path, t0) -> None: txt = t0 or "" digest = hashlib.sha512(db_path.encode("utf-8", "replace")).digest() stackname = ub64enc(digest[:9]).decode("ascii") stackpath = os.path.join(E.cfg, "stack-%s.txt" % (stackname,)) t = " the filesystem at %s may not support locking, or is otherwise incompatible with sqlite\n\n %s\n\n" t += " PS: if you think this is a bug and wish to report it, please include your configuration + the following file: %s\n" txt += t % (db_path, HINT_HISTPATH, stackpath) self.log(txt, 3) try: stk = alltrace() with open(stackpath, "wb") as f: f.write(stk.encode("utf-8", "replace")) except Exception as ex: self.log("warning: failed to write %s: %s" % (stackpath, ex), 3) if self.args.q: t = "-" * 72 raise Exception("%s\n%s\n%s" % (t, txt, t)) def _orz(self, db_path: str) -> "sqlite3.Cursor": assert sqlite3 # type: ignore # !rm c = sqlite3.connect( db_path, timeout=self.timeout, check_same_thread=False ).cursor() # c.connection.set_trace_callback(self._trace) return c def _open_db(self, db_path: str) -> "sqlite3.Cursor": existed = bos.path.exists(db_path) cur = self._orz(db_path) ver = self._read_ver(cur) if not existed and ver is None: return self._try_create_db(db_path, cur) for upver in (4, 5): if ver != upver: continue try: t = "creating backup before upgrade: " cur = self._backup_db(db_path, cur, ver, t) getattr(self, "_upgrade_v%d" % (upver,))(cur) ver += 1 # type: ignore except: self.log("WARN: failed to upgrade from v%d" % (ver,), 3) if ver == DB_VER: # these no longer serve their intended purpose but they're great as additional sanchks self._add_dhash_tab(cur) self._add_xiu_tab(cur) self._add_cv_tab(cur) self._add_idx_up_vp(cur, db_path) self._add_ds_tab(cur) try: nfiles = next(cur.execute("select count(w) from up"))[0] self.log(" {} |{}|".format(db_path, nfiles), "90") return cur except: self.log("WARN: could not list files; DB corrupt?\n" + min_ex()) if (ver or 0) > DB_VER: t = "database is version {}, this copyparty only supports versions <= {}" raise Exception(t.format(ver, DB_VER)) msg = "creating new DB (old is bad); backup: " if ver: msg = "creating new DB (too old to upgrade); backup: " cur = self._backup_db(db_path, cur, ver, msg) db = cur.connection cur.close() db.close() self._delete_db(db_path) return self._try_create_db(db_path, None) def _delete_db(self, db_path: str): for suf in ("", "-shm", "-wal", "-journal"): try: bos.unlink(db_path + suf) except: if not suf: raise def _backup_db( self, db_path: str, cur: "sqlite3.Cursor", ver: Optional[int], msg: str ) -> "sqlite3.Cursor": assert sqlite3 # type: ignore # !rm bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver) self.log(msg + bak) try: c2 = sqlite3.connect(bak) with c2: cur.connection.backup(c2) return cur except: t = "native sqlite3 backup failed; using fallback method:\n" self.log(t + min_ex()) finally: c2.close() # type: ignore db = cur.connection cur.close() db.close() trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak)) return self._orz(db_path) def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]: for tab in ["ki", "kv"]: try: c = cur.execute(r"select v from {} where k = 'sver'".format(tab)) except: continue rows = c.fetchall() if rows: return int(rows[0][0]) return None def _try_create_db( self, db_path: str, cur: Optional["sqlite3.Cursor"] ) -> "sqlite3.Cursor": try: return self._create_db(db_path, cur) except: try: self._delete_db(db_path) except: pass raise def _create_db( self, db_path: str, cur: Optional["sqlite3.Cursor"] ) -> "sqlite3.Cursor": """ collision in 2^(n/2) files where n = bits (6 bits/ch) 10*6/2 = 2^30 = 1'073'741'824, 24.1mb idx 1<<(3*10) 12*6/2 = 2^36 = 68'719'476'736, 24.8mb idx 16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx """ if not cur: cur = self._orz(db_path) idx = r"create index up_w on up(substr(w,1,16))" if self.no_expr_idx: idx = r"create index up_w on up(w)" for cmd in [ r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int, un text)", r"create index up_vp on up(rd, fn)", r"create index up_fn on up(fn)", r"create index up_ip on up(ip)", r"create index up_at on up(at)", idx, r"create table mt (w text, k text, v int)", r"create index mt_w on mt(w)", r"create index mt_k on mt(k)", r"create index mt_v on mt(v)", r"create table kv (k text, v int)", r"insert into kv values ('sver', {})".format(DB_VER), ]: cur.execute(cmd) self._add_dhash_tab(cur) self._add_xiu_tab(cur) self._add_cv_tab(cur) self._add_ds_tab(cur) self.log("created DB at {}".format(db_path)) return cur def _upgrade_v4(self, cur: "sqlite3.Cursor") -> None: for cmd in [ r"alter table up add column ip text", r"alter table up add column at int", r"create index up_ip on up(ip)", r"update kv set v=5 where k='sver'", ]: cur.execute(cmd) cur.connection.commit() def _upgrade_v5(self, cur: "sqlite3.Cursor") -> None: for cmd in [ r"alter table up add column un text", r"update kv set v=6 where k='sver'", ]: cur.execute(cmd) cur.connection.commit() def _add_dhash_tab(self, cur: "sqlite3.Cursor") -> None: # v5 -> v5a try: cur.execute("select d, h from dh limit 1").fetchone() return except: pass for cmd in [ r"create table dh (d text, h text)", r"create index dh_d on dh(d)", r"insert into kv values ('tagscan',1)", ]: cur.execute(cmd) cur.connection.commit() def _add_xiu_tab(self, cur: "sqlite3.Cursor") -> None: # v5a -> v5b # store rd+fn rather than warks to support nohash vols try: cur.execute("select c, w, rd, fn from iu limit 1").fetchone() return except: pass try: cur.execute("drop table iu") except: pass for cmd in [ r"create table iu (c int, w text, rd text, fn text)", r"create index iu_c on iu(c)", r"create index iu_w on iu(w)", ]: cur.execute(cmd) cur.connection.commit() def _add_cv_tab(self, cur: "sqlite3.Cursor") -> None: # v5b -> v5c try: cur.execute("select rd, dn, fn from cv limit 1").fetchone() return except: pass for cmd in [ r"create table cv (rd text, dn text, fn text)", r"create index cv_i on cv(rd, dn)", ]: cur.execute(cmd) try: cur.execute("delete from dh") except: pass cur.connection.commit() def _add_idx_up_vp(self, cur: "sqlite3.Cursor", db_path: str) -> None: # v5c -> v5d try: cur.execute("drop index up_rd") except: return for cmd in [ r"create index up_vp on up(rd, fn)", r"create index up_at on up(at)", ]: self.log("upgrading db [%s]: %s" % (db_path, cmd[:18])) cur.execute(cmd) self.log("upgrading db [%s]: writing to disk..." % (db_path,)) cur.connection.commit() cur.execute("vacuum") def _add_ds_tab(self, cur: "sqlite3.Cursor") -> None: # v5d -> v5e try: cur.execute("select rd, sz from ds limit 1").fetchone() return except: pass for cmd in [ r"create table ds (rd text, sz int, nf int)", r"create index ds_rd on ds(rd)", ]: cur.execute(cmd) cur.connection.commit() def wake_rescanner(self): with self.rescan_cond: self.rescan_cond.notify_all() def handle_json( self, cj: dict[str, Any], busy_aps: dict[str, int] ) -> dict[str, Any]: # busy_aps is u2fh (always undefined if -j0) so this is safe self.busy_aps = busy_aps if self.reload_flag or self.reloading: raise Pebkac(503, SBUSY % ("fs-reload",)) got_lock = False self.fika = "u" try: # bit expensive; 3.9=10x 3.11=2x if self.mutex.acquire(timeout=10): got_lock = True with self.reg_mutex: ret = self._handle_json(cj) else: raise Pebkac(503, SBUSY % (self.blocked or "[unknown]",)) except TypeError: if not PY2: raise with self.mutex, self.reg_mutex: ret = self._handle_json(cj) finally: if got_lock: self.mutex.release() if self.fx_backlog: self.do_fx_backlog() return ret def _handle_json(self, cj: dict[str, Any], depth: int = 1) -> dict[str, Any]: if depth > 16: raise Pebkac(500, "too many xbu relocs, giving up") ptop = cj["ptop"] if not self.register_vpath(ptop, cj["vcfg"]): if ptop not in self.registry: raise Pebkac(410, "location unavailable") cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time() wark = dwark = self._get_wark(cj) job = None pdir = djoin(ptop, cj["prel"]) inc_ap = djoin(pdir, cj["name"]) try: dev = bos.stat(pdir).st_dev except: dev = 0 # check if filesystem supports sparse files; # refuse out-of-order / multithreaded uploading if sprs False sprs = self.fstab.get(pdir)[0] != "ng" if True: jcur = self.cur.get(ptop) reg = self.registry[ptop] vfs = self.vfs.all_vols[cj["vtop"]] n4g = bool(vfs.flags.get("noforget")) noclone = bool(vfs.flags.get("noclone")) rand = vfs.flags.get("rand") or cj.get("rand") lost: list[tuple["sqlite3.Cursor", str, str]] = [] safe_dedup = vfs.flags.get("safededup") or 50 data_ok = safe_dedup < 10 or n4g vols = [(ptop, jcur)] if jcur else [] if vfs.flags.get("xlink"): vols += [(k, v) for k, v in self.cur.items() if k != ptop] if noclone: wark = up2k_wark_from_metadata( self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"] ) zi = cj["lmod"] bad_mt = zi <= 0 or zi > 0xAAAAAAAA if bad_mt or vfs.flags.get("up_ts", "") == "fu": # force upload time rather than last-modified cj["lmod"] = int(time.time()) if zi and bad_mt: t = "ignoring impossible last-modified time from client: %s" self.log(t % (zi,), 6) alts: list[tuple[int, int, dict[str, Any], "sqlite3.Cursor", str, str]] = [] for ptop, cur in vols: allv = self.vfs.all_vols cvfs = next((v for v in allv.values() if v.realpath == ptop), vfs) vtop = cj["vtop"] if cur == jcur else cvfs.vpath if self.no_expr_idx: q = r"select * from up where w = ?" argv = [dwark] else: q = r"select * from up where substr(w,1,16)=? and +w=?" argv = [dwark[:16], dwark] c2 = cur.execute(q, tuple(argv)) for _, dtime, dsize, dp_dir, dp_fn, ip, at, _ in c2: if dp_dir.startswith("//") or dp_fn.startswith("//"): dp_dir, dp_fn = s3dec(dp_dir, dp_fn) dp_abs = djoin(ptop, dp_dir, dp_fn) if noclone and dp_abs != inc_ap: continue try: st = bos.stat(dp_abs) if stat.S_ISLNK(st.st_mode): # broken symlink raise Exception() if st.st_size != dsize: t = "candidate ignored (db/fs desync): {}, size fs={} db={}, mtime fs={} db={}, file: {}" t = t.format( dwark, st.st_size, dsize, st.st_mtime, dtime, dp_abs ) self.log(t) raise Exception() except Exception as ex: if n4g: st = NULLSTAT else: lost.append((cur, dp_dir, dp_fn)) continue j = { "name": dp_fn, "prel": dp_dir, "vtop": vtop, "ptop": ptop, "sprs": sprs, # dontcare; finished anyways "size": dsize, "lmod": dtime, "host": cj["host"], "user": cj["user"], "addr": ip, "at": at, } for k in ["life"]: if k in cj: j[k] = cj[k] # offset of 1st diff in vpaths zig = ( n + 1 for n, (c1, c2) in enumerate( zip(dp_dir + "\r", cj["prel"] + "\n") ) if c1 != c2 ) score = ( (6969 if st.st_dev == dev else 0) + (3210 if dp_dir == cj["prel"] else next(zig)) + (1 if dp_fn == cj["name"] else 0) ) alts.append((score, -len(alts), j, cur, dp_dir, dp_fn)) job = None for dupe in sorted(alts, reverse=True): rj = dupe[2] orig_ap = djoin(rj["ptop"], rj["prel"], rj["name"]) if data_ok or inc_ap == orig_ap: data_ok = True job = rj break else: self.log("asserting contents of %r" % (orig_ap,)) hashes2, st = self._hashlist_from_file(orig_ap) wark2 = up2k_wark_from_hashlist(self.salt, st.st_size, hashes2) if dwark != wark2: t = "will not dedup (fs index desync): fs=%s, db=%s, file: %r\n%s" self.log(t % (wark2, dwark, orig_ap, rj)) lost.append(dupe[3:]) continue data_ok = True job = rj break if job: if wark in reg: del reg[wark] job["hash"] = job["need"] = [] job["done"] = 1 job["busy"] = {} if lost: c2 = None for cur, dp_dir, dp_fn in lost: t = "forgetting desynced db entry: %r" self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn))) self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"]) if c2 and c2 != cur: c2.connection.commit() c2 = cur assert c2 # !rm c2.connection.commit() cur = jcur ptop = None # use cj or job as appropriate if not job and wark in reg: # ensure the files haven't been edited or deleted path = "" st = None rj = reg[wark] names = [rj[x] for x in ["name", "tnam"] if x in rj] for fn in names: path = djoin(rj["ptop"], rj["prel"], fn) try: st = bos.stat(path) if st.st_size > 0 or "done" in rj: # upload completed or both present break except: # missing; restart if not self.args.nw and not n4g: t = "forgetting deleted partial upload at %r" self.log(t % (path,)) del reg[wark] break inc_ap = djoin(cj["ptop"], cj["prel"], cj["name"]) orig_ap = djoin(rj["ptop"], rj["prel"], rj["name"]) if self.args.nw or n4g or not st or "done" not in rj: pass elif st.st_size != rj["size"]: t = "will not dedup (fs index desync): %s, size fs=%d db=%d, mtime fs=%d db=%d, file: %r\n%s" t = t % ( wark, st.st_size, rj["size"], st.st_mtime, rj["lmod"], path, rj, ) self.log(t) del reg[wark] elif inc_ap != orig_ap and not data_ok and "done" in reg[wark]: self.log("asserting contents of %r" % (orig_ap,)) hashes2, _ = self._hashlist_from_file(orig_ap) wark2 = up2k_wark_from_hashlist(self.salt, st.st_size, hashes2) if wark != wark2: t = "will not dedup (fs index desync): fs=%s, idx=%s, file: %r\n%s" self.log(t % (wark2, wark, orig_ap, rj)) del reg[wark] if job or wark in reg: job = job or reg[wark] if ( job["ptop"] != cj["ptop"] or job["prel"] != cj["prel"] or job["name"] != cj["name"] ): # file contents match, but not the path src = djoin(job["ptop"], job["prel"], job["name"]) dst = djoin(cj["ptop"], cj["prel"], cj["name"]) vsrc = djoin(job["vtop"], job["prel"], job["name"]) vsrc = vsrc.replace("\\", "/") # just for prints anyways if vfs.lim: dst, cj["prel"] = vfs.lim.all( cj["addr"], cj["prel"], cj["size"], cj["ptop"], djoin(cj["ptop"], cj["prel"]), self.hub.broker, reg, "up2k._get_volsize", ) bos.makedirs(dst, vf=vfs.flags) vfs.lim.nup(cj["addr"]) vfs.lim.bup(cj["addr"], cj["size"]) if "done" not in job: self.log("unfinished:\n %r\n %r" % (src, dst)) err = "partial upload exists at a different location; please resume uploading here instead:\n" err += "/" + quotep(vsrc) + " " # registry is size-constrained + can only contain one unique wark; # let want_recheck trigger symlink (if still in reg) or reupload if cur: dupe = (cj["prel"], cj["name"], cj["lmod"]) try: if dupe not in self.dupesched[src]: self.dupesched[src].append(dupe) except: self.dupesched[src] = [dupe] raise Pebkac(422, err) elif "nodupe" in vfs.flags: self.log("dupe-reject:\n %r\n %r" % (src, dst)) err = "upload rejected, file already exists:\n" err += "/" + quotep(vsrc) + " " raise Pebkac(409, err) else: # symlink to the client-provided name, # returning the previous upload info psrc = src + ".PARTIAL" if self.args.dotpart: m = re.match(r"(.*[\\/])(.*)", psrc) if m: # always true but... zs1, zs2 = m.groups() psrc = zs1 + "." + zs2 if ( src in self.busy_aps or psrc in self.busy_aps or (wark in reg and "done" not in reg[wark]) ): raise Pebkac( 422, "source file busy; please try again later" ) job = deepcopy(job) job["wark"] = wark job["dwrk"] = dwark job["at"] = cj.get("at") or now zs = "vtop ptop prel name lmod host user addr poke" for k in zs.split(): job[k] = cj.get(k) or "" for k in ("life", "replace"): if k in cj: job[k] = cj[k] pdir = djoin(cj["ptop"], cj["prel"]) if rand: job["name"] = rand_name( pdir, cj["name"], vfs.flags["nrand"] ) dst = djoin(job["ptop"], job["prel"], job["name"]) xbu = vfs.flags.get("xbu") if xbu: vp = djoin(job["vtop"], job["prel"], job["name"]) hr = runhook( self.log, None, self, "xbu.up2k.dupe", xbu, # type: ignore dst, vp, job["host"], job["user"], self.vfs.get_perms(job["vtop"], job["user"]), job["lmod"], job["size"], job["addr"], job["at"], None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" t = t % (vp,) self.log(t, 1) raise Pebkac(403, t) if hr.get("reloc"): x = pathmod(self.vfs, dst, vp, hr["reloc"]) if x: ud1 = (vfs.vpath, job["prel"], job["name"]) pdir, _, job["name"], (vfs, rem) = x dst = os.path.join(pdir, job["name"]) job["vcfg"] = vfs.flags job["ptop"] = vfs.realpath job["vtop"] = vfs.vpath job["prel"] = rem job["name"] = sanitize_fn(job["name"]) ud2 = (vfs.vpath, job["prel"], job["name"]) if ud1 != ud2: # print(json.dumps(job, sort_keys=True, indent=4)) job["hash"] = cj["hash"] self.log("xbu reloc1:%d..." % (depth,), 6) return self._handle_json(job, depth + 1) job["name"] = self._untaken(pdir, job, now) dst = djoin(job["ptop"], job["prel"], job["name"]) if not self.args.nw: dvf: dict[str, Any] = vfs.flags try: dvf = self.flags[job["ptop"]] self._symlink(src, dst, dvf, lmod=cj["lmod"], rm=True) except: if bos.path.exists(dst): wunlink(self.log, dst, dvf) if not n4g: raise if cur and not self.args.nw: zs = "prel name lmod size ptop vtop wark dwrk host user addr at" a = [job[x] for x in zs.split()] self.db_add(cur, vfs.flags, *a) cur.connection.commit() elif wark in reg: # checks out, but client may have hopped IPs job["addr"] = cj["addr"] if not job: ap1 = djoin(cj["ptop"], cj["prel"]) if rand: cj["name"] = rand_name(ap1, cj["name"], vfs.flags["nrand"]) if vfs.lim: ap2, cj["prel"] = vfs.lim.all( cj["addr"], cj["prel"], cj["size"], cj["ptop"], ap1, self.hub.broker, reg, "up2k._get_volsize", ) bos.makedirs(ap2, vf=vfs.flags) vfs.lim.nup(cj["addr"]) vfs.lim.bup(cj["addr"], cj["size"]) job = { "wark": wark, "dwrk": dwark, "t0": now, "sprs": sprs, "hash": deepcopy(cj["hash"]), "need": [], "busy": {}, } # client-provided, sanitized by _get_wark: name, size, lmod zs = "vtop ptop prel name size lmod host user addr poke" for k in zs.split(): job[k] = cj[k] for k in ["life", "replace"]: if k in cj: job[k] = cj[k] # one chunk may occur multiple times in a file; # filter to unique values for the list of missing chunks # (preserve order to reduce disk thrashing) lut = set() for k in cj["hash"]: if k not in lut: job["need"].append(k) lut.add(k) try: ret = self._new_upload(job, vfs, depth) if ret: return ret # xbu recursed except: self.registry[job["ptop"]].pop(job["wark"], None) raise purl = "{}/{}".format(job["vtop"], job["prel"]).strip("/") purl = "/{}/".format(purl) if purl else "/" ret = { "name": job["name"], "purl": purl, "size": job["size"], "lmod": job["lmod"], "sprs": job.get("sprs", sprs), "hash": job["need"], "dwrk": dwark, "wark": wark, } if ( not ret["hash"] and "fk" in vfs.flags and not self.args.nw and (cj["user"] in vfs.axs.uread or cj["user"] in vfs.axs.upget) ): alg = 2 if "fka" in vfs.flags else 1 ap = absreal(djoin(job["ptop"], job["prel"], job["name"])) ino = 0 if ANYWIN else bos.stat(ap).st_ino fk = self.gen_fk(alg, self.args.fk_salt, ap, job["size"], ino) ret["fk"] = fk[: vfs.flags["fk"]] if ( not ret["hash"] and cur and cj.get("umod") and int(cj["lmod"]) != int(job["lmod"]) and not self.args.nw and cj["user"] in vfs.axs.uwrite and cj["user"] in vfs.axs.udel ): sql = "update up set mt=? where substr(w,1,16)=? and +rd=? and +fn=?" try: cur.execute(sql, (cj["lmod"], dwark[:16], job["prel"], job["name"])) cur.connection.commit() ap = djoin(job["ptop"], job["prel"], job["name"]) mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True) self.log("touched %r from %d to %d" % (ap, job["lmod"], mt)) except Exception as ex: self.log("umod failed, %r" % (ex,), 3) return ret def _untaken(self, fdir: str, job: dict[str, Any], ts: float) -> str: fname = job["name"] ip = job["addr"] if self.args.nw: return fname fp = djoin(fdir, fname) ow = job.get("replace") and bos.path.exists(fp) if ow: replace_arg = str(job["replace"]).lower() if ow and "skip" in replace_arg: # type: ignore self.log("skipping upload, filename already exists: %r" % fp) err = "upload rejected, a file with that name already exists" raise Pebkac(409, err) if ow and "mt" in replace_arg: # type: ignore mts = bos.stat(fp).st_mtime mtc = job["lmod"] if mtc < mts: t = "will not overwrite; server %d sec newer than client; %d > %d %r" self.log(t % (mts - mtc, mts, mtc, fp)) ow = False ptop = job["ptop"] vf = self.flags.get(ptop) or {} if ow: self.log("replacing existing file at %r" % (fp,)) cur = None st = bos.stat(fp) try: vrel = vjoin(job["prel"], fname) xlink = bool(vf.get("xlink")) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel) self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink) except Exception as ex: self.log("skipping replace-relink: %r" % (ex,)) finally: if cur: cur.connection.commit() wunlink(self.log, fp, vf) if self.args.plain_ip: dip = ip.replace(":", ".") else: dip = self.hub.iphash.s(ip) f, ret = ren_open( fname, "wb", fdir=fdir, suffix="-%.6f-%s" % (ts, dip), vf=vf, ) f.close() return ret def _symlink( self, src: str, dst: str, flags: dict[str, Any], verbose: bool = True, rm: bool = False, lmod: float = 0, fsrc: Optional[str] = None, is_mv: bool = False, ) -> None: if src == dst or (fsrc and fsrc == dst): t = "symlinking a file to itself?? orig(%s) fsrc(%s) link(%s)" raise Exception(t % (src, fsrc, dst)) if verbose: t = "linking dupe:\n point-to: {0!r}\n link-loc: {1!r}" if fsrc: t += "\n data-src: {2!r}" self.log(t.format(src, dst, fsrc)) if self.args.nw: return linked = 0 try: if rm and bos.path.exists(dst): wunlink(self.log, dst, flags) if not is_mv and not flags.get("dedup"): raise Exception("dedup is disabled in config") if "reflink" in flags: if not USE_FICLONE: raise Exception("reflink") # python 3.14 or newer; no need try: with open(fsenc(src), "rb") as fi, open(fsenc(dst), "wb") as fo: fcntl.ioctl(fo.fileno(), fcntl.FICLONE, fi.fileno()) if "fperms" in flags: set_fperms(fo, flags) except: if bos.path.exists(dst): wunlink(self.log, dst, flags) raise if lmod: bos.utime_c(self.log, dst, int(lmod), False) return lsrc = src ldst = dst fs1 = bos.stat(os.path.dirname(src)).st_dev fs2 = bos.stat(os.path.dirname(dst)).st_dev if fs1 == 0 or fs2 == 0: # py2 on winxp or other unsupported combination raise OSError(errno.ENOSYS, "filesystem does not have st_dev") elif fs1 == fs2: # same fs; make symlink as relative as possible spl = r"[\\/]" if WINDOWS else "/" nsrc = re.split(spl, src) ndst = re.split(spl, dst) nc = 0 for a, b in zip(nsrc, ndst): if a != b: break nc += 1 if nc > 1: zsl = nsrc[nc:] hops = len(ndst[nc:]) - 1 lsrc = "../" * hops + "/".join(zsl) if WINDOWS: lsrc = lsrc.replace("/", "\\") ldst = ldst.replace("/", "\\") try: if "hardlink" in flags: os.link(fsenc(absreal(src)), fsenc(dst)) linked = 2 except Exception as ex: self.log("cannot hardlink: " + repr(ex)) if "hardlinkonly" in flags: raise Exception("symlink-fallback disabled in cfg") if not linked: if ANYWIN: Path(ldst).symlink_to(lsrc) if not bos.path.exists(dst): try: wunlink(self.log, dst, flags) except: pass t = "the created symlink [%s] did not resolve to [%s]" raise Exception(t % (ldst, lsrc)) else: os.symlink(fsenc(lsrc), fsenc(ldst)) linked = 1 except Exception as ex: if str(ex) != "reflink": self.log("cannot link; creating copy: " + repr(ex)) if bos.path.isfile(src): csrc = src elif fsrc and bos.path.isfile(fsrc): csrc = fsrc else: t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)" self.log(t, 1) raise Exception(t % (src, fsrc, dst)) trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst)) if linked < 2 and (linked < 1 or SYMTIME): if lmod: bos.utime_c(self.log, dst, int(lmod), False) if "fperms" in flags: set_ap_perms(dst, flags) def handle_chunks( self, ptop: str, wark: str, chashes: list[str] ) -> tuple[list[str], int, list[list[int]], str, float, int, bool]: self.fika = "u" with self.mutex, self.reg_mutex: self.db_act = self.vol_act[ptop] = time.time() job = self.registry[ptop].get(wark) if not job: known = " ".join([x for x in self.registry[ptop].keys()]) self.log("unknown wark [{}], known: {}".format(wark, known)) raise Pebkac(400, "unknown wark" + SEESLOG) if "t0c" not in job: job["t0c"] = time.time() if len(chashes) > 1 and len(chashes[1]) < 44: # first hash is full-length; expand remaining ones uniq = [] lut = set() for chash in job["hash"]: if chash not in lut: uniq.append(chash) lut.add(chash) try: nchunk = uniq.index(chashes[0]) except: raise Pebkac(400, "unknown chunk0 [%s]" % (chashes[0],)) expanded = [chashes[0]] for prefix in chashes[1:]: nchunk += 1 chash = uniq[nchunk] if not chash.startswith(prefix): t = "next sibling chunk does not start with expected prefix [%s]: [%s]" raise Pebkac(400, t % (prefix, chash)) expanded.append(chash) chashes = expanded for chash in chashes: if chash not in job["need"]: msg = "chash = {} , need:\n".format(chash) msg += "\n".join(job["need"]) self.log(msg) t = "already got that (%s) but thanks??" if chash not in job["hash"]: t = "unknown chunk wtf: %s" raise Pebkac(400, t % (chash,)) if chash in job["busy"]: nh = len(job["hash"]) idx = job["hash"].index(chash) t = "that chunk is already being written to:\n {}\n {} {}/{}\n {}" raise Pebkac(400, t.format(wark, chash, idx, nh, job["name"])) assert chash # type: ignore # !rm chunksize = up2k_chunksize(job["size"]) coffsets = [] nchunks = [] for chash in chashes: nchunk = [n for n, v in enumerate(job["hash"]) if v == chash] if not nchunk: raise Pebkac(400, "unknown chunk %s" % (chash,)) ofs = [chunksize * x for x in nchunk] coffsets.append(ofs) nchunks.append(nchunk) for ofs1, ofs2 in zip(coffsets, coffsets[1:]): gap = (ofs2[0] - ofs1[0]) - chunksize if gap: t = "only sibling chunks can be stitched; gap of %d bytes between offsets %d and %d in %s" raise Pebkac(400, t % (gap, ofs1[0], ofs2[0], job["name"])) path = djoin(job["ptop"], job["prel"], job["tnam"]) if not job["sprs"]: cur_sz = bos.path.getsize(path) if coffsets[0][0] > cur_sz: t = "please upload sequentially using one thread;\nserver filesystem does not support sparse files.\n file: {}\n chunk: {}\n cofs: {}\n flen: {}" t = t.format(job["name"], nchunks[0][0], coffsets[0][0], cur_sz) raise Pebkac(400, t) for chash in chashes: job["busy"][chash] = 1 job["poke"] = time.time() return chashes, chunksize, coffsets, path, job["lmod"], job["size"], job["sprs"] def fast_confirm_chunks( self, ptop: str, wark: str, chashes: list[str], locked: list[str] ) -> tuple[int, str]: if not self.mutex.acquire(False): return -1, "" if not self.reg_mutex.acquire(False): self.mutex.release() return -1, "" try: return self._confirm_chunks(ptop, wark, chashes, locked, False) finally: self.reg_mutex.release() self.mutex.release() def confirm_chunks( self, ptop: str, wark: str, written: list[str], locked: list[str] ) -> tuple[int, str]: self.fika = "u" with self.mutex, self.reg_mutex: return self._confirm_chunks(ptop, wark, written, locked, True) def _confirm_chunks( self, ptop: str, wark: str, written: list[str], locked: list[str], final: bool ) -> tuple[int, str]: if True: self.db_act = self.vol_act[ptop] = time.time() try: job = self.registry[ptop][wark] pdir = djoin(job["ptop"], job["prel"]) src = djoin(pdir, job["tnam"]) dst = djoin(pdir, job["name"]) except Exception as ex: return -2, "confirm_chunk, wark(%r)" % (ex,) # type: ignore for chash in locked if final else written: job["busy"].pop(chash, None) try: for chash in written: job["need"].remove(chash) except Exception as ex: for zs in locked: if job["busy"].pop(zs, None): self.log("panic-unlock wark(%s) chunk(%s)" % (wark, zs), 1) return -2, "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore ret = len(job["need"]) if ret > 0: return ret, src if self.args.nw: self.regdrop(ptop, wark) return ret, dst def finish_upload(self, ptop: str, wark: str, busy_aps: dict[str, int]) -> None: self.busy_aps = busy_aps self.fika = "u" with self.mutex, self.reg_mutex: self._finish_upload(ptop, wark) if self.fx_backlog: self.do_fx_backlog() def _finish_upload(self, ptop: str, wark: str) -> None: """mutex(main,reg) me""" try: job = self.registry[ptop][wark] pdir = djoin(job["ptop"], job["prel"]) src = djoin(pdir, job["tnam"]) dst = djoin(pdir, job["name"]) except Exception as ex: self.log(min_ex(), 1) raise Pebkac(500, "finish_upload, wark, %r%s" % (ex, SEESLOG)) if job["need"]: self.log(min_ex(), 1) t = "finish_upload %s with remaining chunks %s%s" raise Pebkac(500, t % (wark, job["need"], SEESLOG)) upt = job.get("at") or time.time() vflags = self.flags[ptop] atomic_move(self.log, src, dst, vflags) times = (int(time.time()), int(job["lmod"])) t = "no more chunks, setting times %s (%d) on %r" self.log(t % (times, bos.path.getsize(dst), dst)) bos.utime_c(self.log, dst, times[1], False) # the above logmsg (and associated logic) is retained due to unforget.py zs = "prel name lmod size ptop vtop wark dwrk host user addr" z2 = [job[x] for x in zs.split()] wake_sr = False try: flt = job["life"] vfs = self.vfs.all_vols[job["vtop"]] vlt = vfs.flags["lifetime"] if vlt and flt > 1 and flt < vlt: upt -= vlt - flt wake_sr = True t = "using client lifetime; at={:.0f} ({}-{})" self.log(t.format(upt, vlt, flt)) except: pass z2.append(upt) if self.idx_wark(vflags, *z2): del self.registry[ptop][wark] else: for k in "host tnam busy sprs poke".split(): del job[k] job.pop("t0c", None) job["t0"] = int(job["t0"]) job["hash"] = [] job["done"] = 1 self.regdrop(ptop, wark) if wake_sr: with self.rescan_cond: self.rescan_cond.notify_all() dupes = self.dupesched.pop(dst, []) if not dupes: return cur = self.cur.get(ptop) for rd, fn, lmod in dupes: d2 = djoin(ptop, rd, fn) if os.path.exists(d2): continue self._symlink(dst, d2, self.flags[ptop], lmod=lmod) if cur: self.db_add(cur, vflags, rd, fn, lmod, *z2[3:]) if cur: cur.connection.commit() def regdrop(self, ptop: str, wark: str) -> None: """mutex(main,reg) me""" olds = self.droppable[ptop] if wark: olds.append(wark) if len(olds) <= self.args.reg_cap: return n = len(olds) - int(self.args.reg_cap / 2) t = "up2k-registry [{}] has {} droppables; discarding {}" self.log(t.format(ptop, len(olds), n)) for k in olds[:n]: self.registry[ptop].pop(k, None) self.droppable[ptop] = olds[n:] def idx_wark( self, vflags: dict[str, Any], rd: str, fn: str, lmod: float, sz: int, ptop: str, vtop: str, wark: str, dwark: str, host: str, usr: str, ip: str, at: float, skip_xau: bool = False, ) -> bool: cur = self.cur.get(ptop) if not cur: return False self.db_act = self.vol_act[ptop] = time.time() try: self.db_add( cur, vflags, rd, fn, lmod, sz, ptop, vtop, wark, dwark, host, usr, ip, at, skip_xau, ) cur.connection.commit() except Exception as ex: x = self.register_vpath(ptop, {}) assert x # !rm db_ex_chk(self.log, ex, x[1]) raise if "e2t" in self.flags[ptop]: self.tagq.put((ptop, dwark, rd, fn, sz, ip, at)) self.n_tagq += 1 return True def db_rm( self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, sz: int ) -> None: sql = "delete from up where rd = ? and fn = ?" try: r = db.execute(sql, (rd, fn)) except: assert self.mem_cur # !rm r = db.execute(sql, s3enc(self.mem_cur, rd, fn)) if not r.rowcount: return self.volsize[db] -= sz self.volnfiles[db] -= 1 if "nodirsz" not in vflags: try: q = "update ds set nf=nf-1, sz=sz-? where rd=?" while True: db.execute(q, (sz, rd)) if not rd: break rd = rd.rsplit("/", 1)[0] if "/" in rd else "" except: pass def db_add( self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, ts: float, sz: int, ptop: str, vtop: str, wark: str, dwark: str, host: str, usr: str, ip: str, at: float, skip_xau: bool = False, ) -> None: """mutex(main) me""" self.db_rm(db, vflags, rd, fn, sz) if not ip: db_ip = "" else: # plugins may expect this to look like an actual IP db_ip = "1.1.1.1" if "no_db_ip" in vflags else ip sql = "insert into up values (?,?,?,?,?,?,?,?)" v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) try: db.execute(sql, v) except: assert self.mem_cur # !rm rd, fn = s3enc(self.mem_cur, rd, fn) v = (dwark, int(ts), sz, rd, fn, db_ip, int(at or 0), usr) db.execute(sql, v) self.volsize[db] += sz self.volnfiles[db] += 1 xau = False if skip_xau else vflags.get("xau") dst = djoin(ptop, rd, fn) if xau: hr = runhook( self.log, None, self, "xau.up2k", xau, dst, djoin(vtop, rd, fn), host, usr, self.vfs.get_perms(djoin(vtop, rd, fn), usr), ts, sz, ip, at or time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" t = t % (djoin(vtop, rd, fn),) self.log(t, 1) wunlink(self.log, dst, vflags) self.registry[ptop].pop(wark, None) raise Pebkac(403, t) xiu = vflags.get("xiu") if xiu: cds: set[int] = set() for cmd in xiu: m = self.xiu_ptn.search(cmd) cds.add(int(m.group(1)) if m else 5) q = "insert into iu values (?,?,?,?)" for cd in cds: # one for each unique cooldown duration try: db.execute(q, (cd, dwark[:16], rd, fn)) except: assert self.mem_cur # !rm rd, fn = s3enc(self.mem_cur, rd, fn) db.execute(q, (cd, dwark[:16], rd, fn)) if self.xiu_asleep: self.xiu_asleep = False with self.rescan_cond: self.rescan_cond.notify_all() if rd and sz and fn.lower() in self.args.th_coversd_set: # wasteful; db_add will re-index actual covers # but that won't catch existing files crd, cdn = rd.rsplit("/", 1) if "/" in rd else ("", rd) try: q = "select fn from cv where rd=? and dn=?" db_cv = db.execute(q, (crd, cdn)).fetchone()[0] db_lcv = db_cv.lower() if db_lcv in self.args.th_coversd_set: idx_db = self.args.th_coversd.index(db_lcv) idx_fn = self.args.th_coversd.index(fn.lower()) add_cv = idx_fn < idx_db else: add_cv = True except: add_cv = True if add_cv: try: db.execute("delete from cv where rd=? and dn=?", (crd, cdn)) db.execute("insert into cv values (?,?,?)", (crd, cdn, fn)) except: pass if "nodirsz" not in vflags: try: q = "update ds set nf=nf+1, sz=sz+? where rd=?" q2 = "insert into ds values(?,?,1)" while True: if not db.execute(q, (sz, rd)).rowcount: db.execute(q2, (rd, sz)) if not rd: break rd = rd.rsplit("/", 1)[0] if "/" in rd else "" except: pass def handle_fs_abrt(self, akey: str) -> None: self.abrt_key = akey def handle_rm( self, uname: str, ip: str, vpaths: list[str], lim: list[int], rm_up: bool, unpost: bool, ) -> str: n_files = 0 ok = {} ng = {} for vp in vpaths: if lim and lim[0] <= 0: self.log("hit delete limit of {} files".format(lim[1]), 3) break a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up, unpost) n_files += a for k in b: ok[k] = 1 for k in c: ng[k] = 1 ng = {k: 1 for k in ng if k not in ok} iok = len(ok) ing = len(ng) return "deleted {} files (and {}/{} folders)".format(n_files, iok, iok + ing) def _handle_rm( self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool, unpost: bool ) -> tuple[int, list[str], list[str]]: self.db_act = time.time() partial = "" if not unpost: permsets = [[True, False, False, True]] vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0]) vn, rem = vn0.get_dbv(rem0) else: # unpost with missing permissions? verify with db permsets = [[False, True]] vn0, rem0 = self.vfs.get(vpath, uname, *permsets[0]) vn, rem = vn0.get_dbv(rem0) ptop = vn.realpath self.fika = "d" with self.mutex, self.reg_mutex: abrt_cfg = vn.flags.get("u2abort", 1) addr = (ip or "\n") if abrt_cfg in (1, 2) else "" user = ((uname or "\n"), "*") if abrt_cfg in (1, 3) else None reg = self.registry.get(ptop, {}) if abrt_cfg else {} for wark, job in reg.items(): if (addr and addr != job["addr"]) or ( user and job["user"] not in user ): continue jrem = djoin(job["prel"], job["name"]) if ANYWIN: jrem = jrem.replace("\\", "/") if jrem == rem: if job["ptop"] != ptop: t = "job.ptop [%s] != vol.ptop [%s] ??" raise Exception(t % (job["ptop"], ptop)) partial = vn.canonical(vjoin(job["prel"], job["tnam"])) break if partial: dip = ip dat = time.time() dun = uname un_cfg = 1 else: un_cfg = vn.flags["unp_who"] if not self.args.unpost or not un_cfg: t = "the unpost feature is disabled in server config" raise Pebkac(400, t) _, _, _, _, dip, dat, dun = self._find_from_vpath(ptop, rem) t = "you cannot delete this: " if not dip: t += "file not found" elif dip != ip and un_cfg in (1, 2): t += "not uploaded by (You)" elif dun != uname and un_cfg in (1, 3): t += "not uploaded by (You)" elif dat < time.time() - self.args.unpost: t += "uploaded too long ago" else: t = "" if t: raise Pebkac(400, t) ptop = vn.realpath atop = vn.canonical(rem, False) self.vol_act[ptop] = self.db_act adir, fn = os.path.split(atop) try: st = bos.lstat(atop) is_dir = stat.S_ISDIR(st.st_mode) except: # NOTE: "file not found" *sftpd raise Pebkac(400, "file not found on disk (already deleted?)") if "bcasechk" in vn.flags and not vn.casechk(rem, False): raise Pebkac(400, "file does not exist case-sensitively") scandir = not self.args.no_scandir if is_dir: # note: deletion inside shares would require a rewrite here; # shares necessitate get_dbv which is incompatible with walk g = vn0.walk("", rem0, [], uname, permsets, 2, scandir, True) if unpost: raise Pebkac(400, "cannot unpost folders") elif stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode): voldir = vsplit(rem)[0] vpath_dir = vsplit(vpath)[0] g = [(vn, voldir, vpath_dir, adir, [(fn, 0)], [], {})] # type: ignore else: self.log("rm: skip type-0%o file %r" % (st.st_mode, atop)) return 0, [], [] xbd = vn.flags.get("xbd") xad = vn.flags.get("xad") n_files = 0 for dbv, vrem, _, adir, files, rd, vd in g: for fn in [x[0] for x in files]: if lim: lim[0] -= 1 if lim[0] < 0: self.log("hit delete limit of {} files".format(lim[1]), 3) break abspath = djoin(adir, fn) st = stl = bos.lstat(abspath) if stat.S_ISLNK(st.st_mode): try: st = bos.stat(abspath) except: pass volpath = ("%s/%s" % (vrem, fn)).strip("/") vpath = ("%s/%s" % (dbv.vpath, volpath)).strip("/") self.log("rm %r\n %r" % (vpath, abspath)) if not unpost: # recursion-only sanchk _ = dbv.get(volpath, uname, *permsets[0]) if xbd: hr = runhook( self.log, None, self, "xbd", xbd, abspath, vpath, "", uname, self.vfs.get_perms(vpath, uname), stl.st_mtime, st.st_size, ip, time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "delete blocked by xbd server config: %r" % (abspath,) self.log(t, 1) continue n_files += 1 self.fika = "d" with self.mutex, self.reg_mutex: cur = None try: ptop = dbv.realpath xlink = bool(dbv.flags.get("xlink")) cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath) self._forget_file( ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink ) finally: if cur: cur.connection.commit() wunlink(self.log, abspath, dbv.flags) if partial: wunlink(self.log, partial, dbv.flags) partial = "" if xad: runhook( self.log, None, self, "xad", xad, abspath, vpath, "", uname, self.vfs.get_perms(vpath, uname), stl.st_mtime, st.st_size, ip, time.time(), None, ) if is_dir: ok, ng = rmdirs(self.log_func, scandir, True, atop, 1) else: ok = ng = [] if rm_up: ok2, ng2 = rmdirs_up(os.path.dirname(atop), ptop) else: ok2 = ng2 = [] return n_files, ok + ok2, ng + ng2 def handle_cp(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: if svp == dvp or dvp.startswith(svp + "/"): raise Pebkac(400, "cp: cannot copy parent into subfolder") svn, srem = self.vfs.get(svp, uname, True, False) dvn, drem = self.vfs.get(dvp, uname, False, True) svn_dbv, _ = svn.get_dbv(srem) sabs = svn.canonical(srem, False) curs: set["sqlite3.Cursor"] = set() self.db_act = self.vol_act[svn_dbv.realpath] = time.time() st = bos.stat(sabs) if "bcasechk" in svn.flags and not svn.casechk(srem, False): raise Pebkac(400, "file does not exist case-sensitively") if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): self.fika = "c" with self.mutex: try: ret = self._cp_file(uname, ip, svp, dvp, curs) finally: for v in curs: v.connection.commit() return ret if not stat.S_ISDIR(st.st_mode): raise Pebkac(400, "cannot copy type-0%o file" % (st.st_mode,)) permsets = [[True, False]] scandir = not self.args.no_scandir # if user can see dotfiles in target volume, only include # dots from source vols where user also has the dot perm dots = 1 if uname in dvn.axs.udot else 2 # don't use svn_dbv; would skip subvols due to _ls `if not rem:` g = svn.walk("", srem, [], uname, permsets, dots, scandir, True) self.fika = "c" with self.mutex: try: for dbv, vrem, _, atop, files, rd, vd in g: for fn in files: self.db_act = self.vol_act[dbv.realpath] = time.time() svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x) if not svpf.startswith(svp + "/"): # assert self.log(min_ex(), 1) t = "cp: bug at %r, top %r%s" raise Pebkac(500, t % (svpf, svp, SEESLOG)) dvpf = dvp + svpf[len(svp) :] self._cp_file(uname, ip, svpf, dvpf, curs) if abrt and abrt == self.abrt_key: raise Pebkac(400, "filecopy aborted by http-api") for v in curs: v.connection.commit() curs.clear() finally: for v in curs: v.connection.commit() return "k" def _cp_file( self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"] ) -> str: """mutex(main) me; will mutex(reg)""" svn, srem = self.vfs.get(svp, uname, True, False) svn_dbv, srem_dbv = svn.get_dbv(srem) dvn, drem = self.vfs.get(dvp, uname, False, True) dvn, drem = dvn.get_dbv(drem) sabs = svn.canonical(srem, False) dabs = dvn.canonical(drem) drd, dfn = vsplit(drem) if bos.path.exists(dabs): raise Pebkac(400, "cp2: target file exists") st = stl = bos.lstat(sabs) if stat.S_ISLNK(stl.st_mode): is_link = True try: st = bos.stat(sabs) except: pass # broken symlink; keep as-is elif not stat.S_ISREG(st.st_mode): self.log("skipping type-0%o file %r" % (st.st_mode, sabs)) return "" else: is_link = False ftime = stl.st_mtime fsize = st.st_size xbc = svn.flags.get("xbc") xac = dvn.flags.get("xac") if xbc: hr = runhook( self.log, None, self, "xbc", xbc, sabs, svp, "", uname, self.vfs.get_perms(svp, uname), ftime, fsize, ip, time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "copy blocked by xbr server config: %r" % (svp,) self.log(t, 1) raise Pebkac(405, t) bos.makedirs(os.path.dirname(dabs), vf=dvn.flags) c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath( svn_dbv.realpath, srem_dbv ) c2 = self.cur.get(dvn.realpath) if w: assert c1 # !rm if c2 and c2 != c1: self._copy_tags(c1, c2, w) curs.add(c1) if c2: self.db_add( c2, {}, # skip upload hooks drd, dfn, ftime, fsize, dvn.realpath, dvn.vpath, w, w, "", un or "", ip or "", at or 0, ) curs.add(c2) else: self.log("not found in src db: %r" % (svp,)) try: if is_link and st != stl: # relink non-broken symlinks to still work after the move, # but only resolve 1st level to maintain relativity dlink = bos.readlink(sabs) dlink = os.path.join(os.path.dirname(sabs), dlink) dlink = bos.path.abspath(dlink) self._symlink(dlink, dabs, dvn.flags, lmod=ftime) else: self._symlink(sabs, dabs, dvn.flags, lmod=ftime) except OSError as ex: if ex.errno != errno.EXDEV: raise self.log("using plain copy (%s):\n %r\n %r" % (ex.strerror, sabs, dabs)) b1, b2 = fsenc(sabs), fsenc(dabs) is_link = os.path.islink(b1) # due to _relink try: trystat_shutil_copy2(self.log, b1, b2) except: try: wunlink(self.log, dabs, dvn.flags) except: pass if not is_link: raise # broken symlink? keep it as-is try: zb = os.readlink(b1) os.symlink(zb, b2) except: wunlink(self.log, dabs, dvn.flags) raise if is_link: try: times = (int(time.time()), int(ftime)) bos.utime(dabs, times, False) except: pass if "fperms" in dvn.flags: set_ap_perms(dabs, dvn.flags) if xac: runhook( self.log, None, self, "xac", xac, dabs, dvp, "", uname, self.vfs.get_perms(dvp, uname), ftime, fsize, ip, time.time(), None, ) return "k" def handle_mv(self, abrt: str, uname: str, ip: str, svp: str, dvp: str) -> str: if svp == dvp or dvp.startswith(svp + "/"): raise Pebkac(400, "mv: cannot move parent into subfolder") svn, srem = self.vfs.get(svp, uname, True, False, True) jail, jail_rem = svn.get_dbv(srem) sabs = svn.canonical(srem, False) curs: set["sqlite3.Cursor"] = set() self.db_act = self.vol_act[jail.realpath] = time.time() if not jail_rem: raise Pebkac(400, "mv: cannot move a mountpoint") st = bos.lstat(sabs) if "bcasechk" in svn.flags and not svn.casechk(srem, False): raise Pebkac(400, "file does not exist case-sensitively") if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): self.fika = "m" with self.mutex: try: ret = self._mv_file(uname, ip, svp, dvp, curs) finally: for v in curs: v.connection.commit() return ret if not stat.S_ISDIR(st.st_mode): raise Pebkac(400, "cannot move type-0%o file" % (st.st_mode,)) permsets = [[True, False, True]] scandir = not self.args.no_scandir # following symlinks is too scary g = svn.walk("", srem, [], uname, permsets, 2, scandir, True) for dbv, vrem, _, atop, files, rd, vd in g: if dbv != jail: # fail early (prevent partial moves) raise Pebkac(400, "mv: source folder contains other volumes") g = svn.walk("", srem, [], uname, permsets, 2, scandir, True) self.fika = "m" with self.mutex: try: for dbv, vrem, _, atop, files, rd, vd in g: if dbv != jail: # the actual check (avoid toctou) raise Pebkac(400, "mv: source folder contains other volumes") for fn in files: self.db_act = self.vol_act[dbv.realpath] = time.time() svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x) if not svpf.startswith(svp + "/"): # assert self.log(min_ex(), 1) t = "mv: bug at %r, top %r%s" raise Pebkac(500, t % (svpf, svp, SEESLOG)) dvpf = dvp + svpf[len(svp) :] self._mv_file(uname, ip, svpf, dvpf, curs) if abrt and abrt == self.abrt_key: raise Pebkac(400, "filemove aborted by http-api") for v in curs: v.connection.commit() curs.clear() finally: for v in curs: v.connection.commit() rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1) for zsl in (rm_ok, rm_ng): for ap in reversed(zsl): if not ap.startswith(sabs): self.log(min_ex(), 1) t = "mv_d: bug at %r, top %r%s" raise Pebkac(500, t % (ap, sabs, SEESLOG)) rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/") vp = vjoin(dvp, rem) try: dvn, drem = self.vfs.get(vp, uname, False, True) 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 return "k" def _mv_file( self, uname: str, ip: str, svp: str, dvp: str, curs: set["sqlite3.Cursor"] ) -> str: """mutex(main) me; will mutex(reg)""" svn, srem = self.vfs.get(svp, uname, True, False, True) svn, srem = svn.get_dbv(srem) dvn, drem = self.vfs.get(dvp, uname, False, True) dvn, drem = dvn.get_dbv(drem) sabs = svn.canonical(srem, False) dabs = dvn.canonical(drem) drd, dfn = vsplit(drem) n1 = svp.split("/")[-1] n2 = dvp.split("/")[-1] if n1.startswith(".") or n2.startswith("."): if self.args.no_dot_mv: raise Pebkac(400, "moving dotfiles is disabled in server config") elif self.args.no_dot_ren and n1 != n2: raise Pebkac(400, "renaming dotfiles is disabled in server config") if bos.path.exists(dabs): raise Pebkac(400, "mv2: target file exists") is_link = is_dirlink = False st = stl = bos.lstat(sabs) if stat.S_ISLNK(stl.st_mode): is_link = True try: st = bos.stat(sabs) is_dirlink = stat.S_ISDIR(st.st_mode) except: pass # broken symlink; keep as-is ftime = stl.st_mtime fsize = st.st_size xbr = svn.flags.get("xbr") xar = dvn.flags.get("xar") if xbr: hr = runhook( self.log, None, self, "xbr", xbr, sabs, svp, "", uname, self.vfs.get_perms(svp, uname), ftime, fsize, ip, time.time(), None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "move blocked by xbr server config: %r" % (svp,) self.log(t, 1) raise Pebkac(405, t) is_xvol = svn.realpath != dvn.realpath bos.makedirs(os.path.dirname(dabs), vf=dvn.flags) if is_dirlink: dlabs = absreal(sabs) t = "moving symlink from %r to %r, target %r" self.log(t % (sabs, dabs, dlabs)) mt = bos.path.getmtime(sabs, False) wunlink(self.log, sabs, svn.flags) self._symlink(dlabs, dabs, dvn.flags, False, lmod=mt) # folders are too scary, schedule rescan of both vols self.need_rescan.add(svn.vpath) self.need_rescan.add(dvn.vpath) with self.rescan_cond: self.rescan_cond.notify_all() if xar: runhook( self.log, None, self, "xar.ln", xar, dabs, dvp, "", uname, self.vfs.get_perms(dvp, uname), ftime, fsize, ip, time.time(), None, ) return "k" c1, w, ftime_, fsize_, ip, at, un = self._find_from_vpath(svn.realpath, srem) c2 = self.cur.get(dvn.realpath) has_dupes = False if w: assert c1 # !rm if c2 and c2 != c1: if "nodupem" in dvn.flags: q = "select w from up where substr(w,1,16) = ?" for (w2,) in c2.execute(q, (w[:16],)): if w == w2: t = "file exists in target volume, and dupes are forbidden in config" raise Pebkac(400, t) self._copy_tags(c1, c2, w) xlink = bool(svn.flags.get("xlink")) with self.reg_mutex: has_dupes = self._forget_file( svn.realpath, srem, svn.flags, c1, w, is_xvol, fsize_ or fsize, xlink, ) if not is_xvol: has_dupes = self._relink(w, svn.realpath, srem, dabs, c1, xlink) curs.add(c1) if c2: self.db_add( c2, {}, # skip upload hooks drd, dfn, ftime, fsize, dvn.realpath, dvn.vpath, w, w, "", un or "", ip or "", at or 0, ) curs.add(c2) else: self.log("not found in src db: %r" % (svp,)) try: if is_xvol and has_dupes: raise OSError(errno.EXDEV, "src is symlink") if is_link and st != stl: # relink non-broken symlinks to still work after the move, # but only resolve 1st level to maintain relativity dlink = bos.readlink(sabs) dlink = os.path.join(os.path.dirname(sabs), dlink) dlink = bos.path.abspath(dlink) self._symlink(dlink, dabs, dvn.flags, lmod=ftime, is_mv=True) wunlink(self.log, sabs, svn.flags) else: atomic_move(self.log, sabs, dabs, svn.flags) if svn != dvn and "fperms" in dvn.flags: set_ap_perms(dabs, dvn.flags) except OSError as ex: if ex.errno != errno.EXDEV: raise self.log("using copy+delete (%s):\n %r\n %r" % (ex.strerror, sabs, dabs)) b1, b2 = fsenc(sabs), fsenc(dabs) is_link = os.path.islink(b1) # due to _relink try: trystat_shutil_copy2(self.log, b1, b2) except: try: wunlink(self.log, dabs, dvn.flags) except: pass if not is_link: raise # broken symlink? keep it as-is try: zb = os.readlink(b1) os.symlink(zb, b2) except: wunlink(self.log, dabs, dvn.flags) raise if is_link: try: times = (int(time.time()), int(ftime)) bos.utime(dabs, times, False) except: pass if "fperms" in dvn.flags: set_ap_perms(dabs, dvn.flags) wunlink(self.log, sabs, svn.flags) if xar: runhook( self.log, None, self, "xar.mv", xar, dabs, dvp, "", uname, self.vfs.get_perms(dvp, uname), ftime, fsize, ip, time.time(), None, ) return "k" def _copy_tags( self, csrc: "sqlite3.Cursor", cdst: "sqlite3.Cursor", wark: str ) -> None: """copy all tags for wark from src-db to dst-db""" w = wark[:16] if cdst.execute("select * from mt where w=? limit 1", (w,)).fetchone(): return # existing tags in dest db for _, k, v in csrc.execute("select * from mt where w=?", (w,)): cdst.execute("insert into mt values(?,?,?)", (w, k, v)) def _find_from_vpath( self, ptop: str, vrem: str ) -> tuple[ Optional["sqlite3.Cursor"], Optional[str], Optional[int], Optional[int], str, Optional[int], str, ]: cur = self.cur.get(ptop) if not cur: return None, None, None, None, "", None, "" rd, fn = vsplit(vrem) q = "select w, mt, sz, ip, at, un from up where rd=? and fn=? limit 1" try: c = cur.execute(q, (rd, fn)) except: assert self.mem_cur # !rm c = cur.execute(q, s3enc(self.mem_cur, rd, fn)) hit = c.fetchone() if hit: wark, ftime, fsize, ip, at, un = hit return cur, wark, ftime, fsize, ip, at, un return cur, None, None, None, "", None, "" def _forget_file( self, ptop: str, vrem: str, vflags: dict[str, Any], cur: Optional["sqlite3.Cursor"], wark: Optional[str], drop_tags: bool, sz: int, xlink: bool, ) -> bool: """ mutex(main,reg) me forgets file in db, fixes symlinks, does not delete """ srd, sfn = vsplit(vrem) has_dupes = False self.log("forgetting %r" % (vrem,)) if wark and cur: self.log("found {} in db".format(wark)) if drop_tags: if self._relink(wark, ptop, vrem, "", cur, xlink): has_dupes = True drop_tags = False if drop_tags: q = "delete from mt where w=?" cur.execute(q, (wark[:16],)) self.db_rm(cur, vflags, srd, sfn, sz) reg = self.registry.get(ptop) if reg: vdir = vsplit(vrem)[0] wark = wark or next( ( x for x, y in reg.items() if sfn in [y["name"], y.get("tnam")] and y["prel"] == vdir ), "", ) job = reg.get(wark) if wark else None if job: if job["need"]: t = "forgetting partial upload {} ({})" p = self._vis_job_progress(job) self.log(t.format(wark, p)) src = djoin(ptop, vrem) zi = len(self.dupesched.pop(src, [])) if zi: t = "...and forgetting %d links in dupesched" self.log(t % (zi,)) assert wark del reg[wark] return has_dupes def _relink( self, wark: str, sptop: str, srem: str, dabs: str, vcur: Optional["sqlite3.Cursor"], xlink: bool, ) -> int: """ update symlinks from file at svn/srem to dabs (rename), or to first remaining full if no dabs (delete) """ dupes = [] sabs = djoin(sptop, srem) if self.no_expr_idx: q = r"select rd, fn from up where w = ?" argv = (wark,) else: q = r"select rd, fn from up where substr(w,1,16)=? and +w=?" argv = (wark[:16], wark) for ptop, cur in self.cur.items(): if not xlink and cur and cur != vcur: continue for rd, fn in cur.execute(q, argv): if rd.startswith("//") or fn.startswith("//"): rd, fn = s3dec(rd, fn) dvrem = vjoin(rd, fn).strip("/") if ptop != sptop or srem != dvrem: dupes.append([ptop, dvrem]) self.log("found %s dupe: %r %r" % (wark, ptop, dvrem)) if not dupes: return 0 full: dict[str, tuple[str, str]] = {} links: dict[str, tuple[str, str]] = {} for ptop, vp in dupes: ap = djoin(ptop, vp) try: d = links if bos.path.islink(ap) else full d[ap] = (ptop, vp) except: self.log("relink: not found: %r" % (ap,)) # self.log("full:\n" + "\n".join(" {:90}: {}".format(*x) for x in full.items())) # self.log("links:\n" + "\n".join(" {:90}: {}".format(*x) for x in links.items())) if not dabs and not full and links: # deleting final remaining full copy; swap it with a symlink slabs = list(sorted(links.keys()))[0] ptop, rem = links.pop(slabs) self.log("linkswap %r and %r" % (sabs, slabs)) mt = bos.path.getmtime(slabs, False) flags = self.flags.get(ptop) or {} atomic_move(self.log, sabs, slabs, flags) try: bos.utime(slabs, (int(time.time()), int(mt)), False) except: self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3) self._symlink(slabs, sabs, flags, False, is_mv=True) full[slabs] = (ptop, rem) sabs = slabs if not dabs: dabs = list(sorted(full.keys()))[0] for alink, parts in links.items(): lmod = 0.0 try: faulty = False ldst = alink try: for n in range(40): # MAXSYMLINKS zs = bos.readlink(ldst) ldst = os.path.join(os.path.dirname(ldst), zs) ldst = bos.path.abspath(ldst) if not bos.path.islink(ldst): break if ldst == sabs: t = "relink because level %d would break:" self.log(t % (n,), 6) faulty = True except Exception as ex: self.log("relink because walk failed: %s; %r" % (ex, ex), 3) faulty = True zs = absreal(alink) if ldst != zs: t = "relink because computed != actual destination:\n %r\n %r" self.log(t % (ldst, zs), 3) ldst = zs faulty = True if bos.path.islink(ldst): raise Exception("broken symlink: %s" % (alink,)) if alink != sabs and ldst != sabs and not faulty: continue # original symlink OK; leave it be except Exception as ex: t = "relink because symlink verification failed: %s; %r" self.log(t % (ex, ex), 3) self.log("relinking %r to %r" % (alink, dabs)) flags = self.flags.get(parts[0]) or {} try: lmod = bos.path.getmtime(alink, False) wunlink(self.log, alink, flags) except: pass # this creates a link pointing from dabs to alink; alink may # not exist yet, which becomes problematic if the symlinking # fails and it has to fall back on hardlinking/copying files # (for example a volume with symlinked dupes but no --dedup); # fsrc=sabs is then a source that currently resolves to copy self._symlink( dabs, alink, flags, False, lmod=lmod or 0, fsrc=sabs, is_mv=True ) return len(full) + len(links) def _get_wark(self, cj: dict[str, Any]) -> str: if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB raise Pebkac(400, "name or numchunks not according to spec") for k in cj["hash"]: if not self.r_hash.match(k): raise Pebkac( 400, "at least one hash is not according to spec: %r" % (k,) ) # try to use client-provided timestamp, don't care if it fails somehow try: cj["lmod"] = int(cj["lmod"]) except: cj["lmod"] = int(time.time()) if cj["hash"]: wark = up2k_wark_from_hashlist(self.salt, cj["size"], cj["hash"]) else: wark = up2k_wark_from_metadata( self.salt, cj["size"], cj["lmod"], cj["prel"], cj["name"] ) return wark def _hashlist_from_file( self, path: str, prefix: str = "" ) -> tuple[list[str], os.stat_result]: st = bos.stat(path) fsz = st.st_size csz = up2k_chunksize(fsz) ret = [] suffix = " MB, {}".format(path) with open(fsenc(path), "rb", self.args.iobuf) as f: if self.mth and fsz >= 1024 * 512: tlt = self.mth.hash(f, fsz, csz, self.pp, prefix, suffix) ret = [x[0] for x in tlt] fsz = 0 while fsz > 0: # same as `hash_at` except for `imutex` / bufsz if self.stop: return [], st if self.pp: mb = fsz // (1024 * 1024) self.pp.msg = prefix + str(mb) + suffix hashobj = hashlib.sha512() rem = min(csz, fsz) fsz -= rem while rem > 0: buf = f.read(min(rem, 64 * 1024)) if not buf: raise Exception("EOF at " + str(f.tell())) hashobj.update(buf) rem -= len(buf) digest = hashobj.digest()[:33] ret.append(ub64enc(digest).decode("ascii")) return ret, st def _ex_hash(self, ex: Exception, ap: str) -> None: eno = getattr(ex, "errno", 0) if eno in E_FS_MEH: return self.log("hashing failed; %r @ %r" % (ex, ap)) if eno not in E_FS_CRIT: return self.log("hashing failed; %r @ %r\n%s" % (ex, ap, min_ex()), 3) t = "hashing failed; %r @ %r\n%s\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\n" t2 = "" if ANYWIN or MACOS else "dmesg and " return self.log(t % (ex, ap, min_ex(), t2), 1) def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]: pdir = djoin(job["ptop"], job["prel"]) if not job["size"]: try: inf = bos.stat(djoin(pdir, job["name"])) if stat.S_ISREG(inf.st_mode): job["lmod"] = inf.st_size return {} except: pass vf = self.flags[job["ptop"]] xbu = vf.get("xbu") ap_chk = djoin(pdir, job["name"]) vp_chk = djoin(job["vtop"], job["prel"], job["name"]) if xbu: hr = runhook( self.log, None, self, "xbu.up2k", xbu, ap_chk, vp_chk, job["host"], job["user"], self.vfs.get_perms(vp_chk, job["user"]), job["lmod"], job["size"], job["addr"], job["t0"], None, ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vp_chk,) self.log(t, 1) raise Pebkac(403, t) if hr.get("reloc"): x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"]) if x: ud1 = (vfs.vpath, job["prel"], job["name"]) pdir, _, job["name"], (vfs, rem) = x job["vcfg"] = vf = vfs.flags job["ptop"] = vfs.realpath job["vtop"] = vfs.vpath job["prel"] = rem job["name"] = sanitize_fn(job["name"]) ud2 = (vfs.vpath, job["prel"], job["name"]) if ud1 != ud2: self.log("xbu reloc2:%d..." % (depth,), 6) return self._handle_json(job, depth + 1) job["name"] = self._untaken(pdir, job, job["t0"]) self.registry[job["ptop"]][job["wark"]] = job tnam = job["name"] + ".PARTIAL" if self.args.dotpart: tnam = "." + tnam if self.args.nw: job["tnam"] = tnam if not job["hash"]: del self.registry[job["ptop"]][job["wark"]] return {} if self.args.plain_ip: dip = job["addr"].replace(":", ".") else: dip = self.hub.iphash.s(job["addr"]) f, job["tnam"] = ren_open( tnam, "wb", fdir=pdir, suffix="-%.6f-%s" % (job["t0"], dip), vf=vf, ) try: abspath = djoin(pdir, job["tnam"]) sprs = job["sprs"] sz = job["size"] relabel = False if ( ANYWIN and sprs and self.args.sparse and self.args.sparse * 1024 * 1024 <= sz ): try: sp.check_call(["fsutil", "sparse", "setflag", abspath]) except: self.log("could not sparse %r" % (abspath,), 3) relabel = True sprs = False if not ANYWIN and sprs and sz > 1024 * 1024: fs, mnt = self.fstab.get(pdir) if fs == "ok": pass elif "nosparse" in vf: t = "volflag 'nosparse' is preventing creation of sparse files for uploads to [%s]" self.log(t % (job["ptop"],)) relabel = True sprs = False elif "sparse" in vf: t = "volflag 'sparse' is forcing creation of sparse files for uploads to [%s]" self.log(t % (job["ptop"],)) relabel = True else: relabel = True f.seek(1024 * 1024 - 1) f.write(b"e") f.flush() try: nblk = bos.stat(abspath).st_blocks sprs = nblk < 2048 except: sprs = False if relabel: t = "sparse files {} on {} filesystem at {}" nv = "ok" if sprs else "ng" self.log(t.format(nv, self.fstab.get(pdir), pdir)) self.fstab.relabel(pdir, nv) job["sprs"] = sprs if job["hash"] and sprs: f.seek(sz - 1) f.write(b"e") finally: f.close() if not job["hash"]: self._finish_upload(job["ptop"], job["wark"]) return {} def _snapshot(self) -> None: slp = self.args.snap_wri if not slp or self.args.no_snap: return while True: time.sleep(slp) if self.pp: slp = 5 else: slp = self.args.snap_wri self.do_snapshot() def do_snapshot(self) -> None: self.fika = "u" with self.mutex, self.reg_mutex: for k, reg in self.registry.items(): self._snap_reg(k, reg) def _snap_reg(self, ptop: str, reg: dict[str, dict[str, Any]]) -> None: now = time.time() histpath = self.vfs.dbpaths.get(ptop) if not histpath: return idrop = self.args.snap_drop * 60 rm = [x for x in reg.values() if x["need"] and now - x["poke"] >= idrop] if self.args.nw: lost = [] else: lost = [ x for x in reg.values() if x["need"] and not bos.path.exists(djoin(x["ptop"], x["prel"], x["name"])) ] if rm or lost: t = "dropping {} abandoned, {} deleted uploads in {}" t = t.format(len(rm), len(lost), ptop) rm.extend(lost) vis = [self._vis_job_progress(x) for x in rm] self.log("\n".join([t] + vis)) for job in rm: del reg[job["wark"]] rsv_cleared = False try: # remove the filename reservation path = djoin(job["ptop"], job["prel"], job["name"]) if bos.path.getsize(path) == 0: bos.unlink(path) rsv_cleared = True except: pass try: if len(job["hash"]) == len(job["need"]) or ( rsv_cleared and "rm_partial" in self.flags[job["ptop"]] ): # PARTIAL is empty (hash==need) or --rm-partial, so delete that too path = djoin(job["ptop"], job["prel"], job["tnam"]) bos.unlink(path) except: pass if self.args.nw or self.args.no_snap: return path = os.path.join(histpath, "up2k.snap") if not reg: if ptop not in self.snap_prev or self.snap_prev[ptop] is not None: self.snap_prev[ptop] = None if bos.path.exists(path): bos.unlink(path) return newest = float( max(x["t0"] if "done" in x else x["poke"] for _, x in reg.items()) if reg else 0 ) etag = (len(reg), newest) if etag == self.snap_prev.get(ptop): return if bos.makedirs(histpath): hidedir(histpath) path2 = "{}.{}".format(path, os.getpid()) body = {"droppable": self.droppable[ptop], "registry": reg} j = json.dumps(body, sort_keys=True, separators=(",\n", ": ")).encode("utf-8") # j = re.sub(r'"(need|hash)": \[\],\n', "", j) # bytes=slow, utf8=hungry j = j.replace(b'"need": [],\n', b"") # surprisingly optimal j = j.replace(b'"hash": [],\n', b"") with gzip.GzipFile(path2, "wb") as f: f.write(j) atomic_move(self.log, path2, path, VF_CAREFUL) self.log("snap: %s |%d| %.2fs" % (path, len(reg), time.time() - now)) self.snap_prev[ptop] = etag def _tagger(self) -> None: with self.mutex: self.n_tagq += 1 assert self.mtag while True: with self.mutex: self.n_tagq -= 1 ptop, wark, rd, fn, sz, ip, at = self.tagq.get() if "e2t" not in self.flags[ptop]: continue # self.log("\n " + repr([ptop, rd, fn])) abspath = djoin(ptop, rd, fn) try: tags = self.mtag.get(abspath, self.flags[ptop]) if sz else {} ntags1 = len(tags) parsers = self._get_parsers(ptop, tags, abspath) if self.args.mtag_vv: t = "parsers({}): {}\n{} {} tags: {}".format( ptop, list(parsers.keys()), ntags1, self.mtag.backend, tags ) self.log(t) if parsers: tags["up_ip"] = ip tags["up_at"] = at tags.update(self.mtag.get_bin(parsers, abspath, tags)) except Exception as ex: self._log_tag_err("", abspath, ex) continue with self.mutex: cur = self.cur[ptop] if not cur: self.log("no cursor to write tags with??", c=1) continue # TODO is undef if vol 404 on startup entags = self.entags.get(ptop) if not entags: self.log("no entags okay.jpg", c=3) continue self._tag_file(cur, entags, wark, abspath, tags) cur.connection.commit() self.log("tagged %r (%d+%d)" % (abspath, ntags1, len(tags) - ntags1)) def _hasher(self) -> None: with self.hashq_mutex: self.n_hashq += 1 while True: with self.hashq_mutex: self.n_hashq -= 1 # self.log("hashq {}".format(self.n_hashq)) task = self.hashq.get() if len(task) != 9: raise Exception("invalid hash task") try: if not self._hash_t(task) and self.stop: return except Exception as ex: self.log("failed to hash %r: %s" % (task, ex), 1) def _hash_t( self, task: tuple[str, str, dict[str, Any], str, str, str, float, str, bool] ) -> bool: ptop, vtop, flags, rd, fn, ip, at, usr, skip_xau = task # self.log("hashq {} pop {}/{}/{}".format(self.n_hashq, ptop, rd, fn)) with self.mutex, self.reg_mutex: if not self.register_vpath(ptop, flags): return True abspath = djoin(ptop, rd, fn) self.log("hashing %r" % (abspath,)) inf = bos.stat(abspath) if not inf.st_size: wark = up2k_wark_from_metadata( self.salt, inf.st_size, int(inf.st_mtime), rd, fn ) else: hashes, _ = self._hashlist_from_file(abspath) if not hashes: return False wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes) with self.mutex, self.reg_mutex: self.idx_wark( self.flags[ptop], rd, fn, inf.st_mtime, inf.st_size, ptop, vtop, wark, wark, "", usr, ip, at, skip_xau, ) if at and time.time() - at > 30: with self.rescan_cond: self.rescan_cond.notify_all() if self.fx_backlog: self.do_fx_backlog() return True def hash_file( self, ptop: str, vtop: str, flags: dict[str, Any], rd: str, fn: str, ip: str, at: float, usr: str, skip_xau: bool = False, ) -> None: if "e2d" not in flags: return if self.n_hashq > 1024: t = "%d files in hashq; taking a nap" self.log(t % (self.n_hashq,), 6) for _ in range(self.n_hashq // 1024): time.sleep(0.1) if self.n_hashq < 1024: break zt = (ptop, vtop, flags, rd, fn, ip, at, usr, skip_xau) with self.hashq_mutex: self.hashq.put(zt) self.n_hashq += 1 def do_fx_backlog(self): with self.mutex, self.reg_mutex: todo = self.fx_backlog self.fx_backlog = [] for act, hr, req_vp in todo: self.hook_fx(act, hr, req_vp) def hook_fx(self, act: str, hr: dict[str, str], req_vp: str) -> None: bad = [k for k in hr if k != "vp"] if bad: t = "got unsupported key in %s from hook: %s" raise Exception(t % (act, bad)) for fvp in hr.get("vp") or []: # expect vpath including filename; either absolute # or relative to the client's vpath (request url) if fvp.startswith("/"): fvp, fn = vsplit(fvp[1:]) fvp = "/" + fvp else: fvp, fn = vsplit(fvp) x = pathmod(self.vfs, "", req_vp, {"vp": fvp, "fn": fn}) if not x: t = "hook_fx(%s): failed to resolve %r based on %r" self.log(t % (act, fvp, req_vp)) continue ap, rd, fn, (vn, rem) = x vp = vjoin(rd, fn) if not vp: raise Exception("hook_fx: blank vp from pathmod") if act == "idx": rd = rd[len(vn.vpath) :].strip("/") self.hash_file( vn.realpath, vn.vpath, vn.flags, rd, fn, "", time.time(), "", True ) if act == "del": self._handle_rm(LEELOO_DALLAS, "", vp, [], False, False) def shutdown(self) -> None: self.stop = True self.fika = "f" if self.mth: self.mth.stop = True # in case we're killed early for x in list(self.spools): self._unspool(x) if not self.args.no_snap: self.log("writing snapshot") self.do_snapshot() t0 = time.time() while self.pp: time.sleep(0.1) if time.time() - t0 >= 1: break # if there is time for x in list(self.spools): self._unspool(x) for cur in self.cur.values(): db = cur.connection try: db.interrupt() except: pass cur.close() db.close() if self.mem_cur: db = self.mem_cur.connection self.mem_cur.close() db.close() self.registry = {} def up2k_chunksize(filesize: int) -> int: chunksize = 1024 * 1024 stepsize = 512 * 1024 while True: for mul in [1, 2]: nchunks = math.ceil(filesize * 1.0 / chunksize) if nchunks <= 256 or (chunksize >= 32 * 1024 * 1024 and nchunks <= 4096): return chunksize chunksize += stepsize stepsize *= mul def up2k_wark_from_hashlist(salt: str, filesize: int, hashes: list[str]) -> str: """server-reproducible file identifier, independent of name or location""" values = [salt, str(filesize)] + hashes vstr = "\n".join(values) wark = hashlib.sha512(vstr.encode("utf-8")).digest()[:33] return ub64enc(wark).decode("ascii") def up2k_wark_from_metadata(salt: str, sz: int, lastmod: int, rd: str, fn: str) -> str: ret = sfsenc("%s\n%d\n%d\n%s\n%s" % (salt, lastmod, sz, rd, fn)) ret = ub64enc(hashlib.sha512(ret).digest()) return ("#%s" % (ret.decode("ascii"),))[:44] ================================================ FILE: copyparty/util.py ================================================ # coding: utf-8 from __future__ import print_function, unicode_literals import argparse import base64 import binascii import codecs import errno import hashlib import hmac import json import logging import math import mimetypes import os import platform import re import select import shutil import signal import socket import stat import struct import subprocess as sp # nosec import sys import threading import time import traceback from collections import Counter from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from queue import Queue try: from zlib_ng import gzip_ng as gzip from zlib_ng import zlib_ng as zlib sys.modules["gzip"] = gzip # sys.modules["zlib"] = zlib # `- somehow makes tarfile 3% slower with default malloc, and barely faster with mimalloc except: import gzip import zlib from .__init__ import ( ANYWIN, EXE, GRAAL, MACOS, PY2, PY36, TYPE_CHECKING, VT100, WINDOWS, EnvParams, unicode, ) from .__version__ import S_BUILD_DT, S_VERSION def noop(*a, **ka): pass try: from datetime import datetime, timezone UTC = timezone.utc except: from datetime import datetime, timedelta, tzinfo TD_ZERO = timedelta(0) class _UTC(tzinfo): def utcoffset(self, dt): return TD_ZERO def tzname(self, dt): return "UTC" def dst(self, dt): return TD_ZERO UTC = _UTC() if PY2: range = xrange # type: ignore from .stolen import surrogateescape surrogateescape.register_surrogateescape() if sys.version_info >= (3, 7) or ( PY36 and platform.python_implementation() == "CPython" ): ODict = dict else: from collections import OrderedDict as ODict def _ens(want: str) -> tuple[int, ...]: ret: list[int] = [] for v in want.split(): try: ret.append(getattr(errno, v)) except: pass return tuple(ret) # WSAECONNRESET - foribly closed by remote # WSAENOTSOCK - no longer a socket # EUNATCH - can't assign requested address (wifi down) E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET") E_SCK_WR = _ens("EPIPE ESHUTDOWN EBADFD") E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL") E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE") E_ACCESS = _ens("EACCES WSAEACCES") E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH") E_FS_MEH = _ens("EPERM EACCES ENOENT ENOTCAPABLE") E_FS_CRIT = _ens("EIO EFAULT EUCLEAN ENOTBLK") IP6ALL = "0:0:0:0:0:0:0:0" IP6_LL = ("fe8", "fe9", "fea", "feb") IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254") UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._" BC_CDISP = UC_CDISP.encode("ascii") UC_CDISP_SET = set(UC_CDISP) BC_CDISP_SET = set(BC_CDISP) try: import fcntl HAVE_FCNTL = True HAVE_FICLONE = hasattr(fcntl, "FICLONE") except: HAVE_FCNTL = False HAVE_FICLONE = False try: import termios except: pass try: if os.environ.get("PRTY_NO_CTYPES"): raise Exception() import ctypes except: ctypes = None try: wk32 = ctypes.WinDLL(str("kernel32"), use_last_error=True) # type: ignore except: wk32 = None try: if os.environ.get("PRTY_NO_IFADDR"): raise Exception() try: if os.environ.get("PRTY_SYS_ALL") or os.environ.get("PRTY_SYS_IFADDR"): raise ImportError() from .stolen.ifaddr import get_adapters except ImportError: from ifaddr import get_adapters HAVE_IFADDR = True except: HAVE_IFADDR = False def get_adapters(include_unconfigured=False): return [] try: if os.environ.get("PRTY_NO_SQLITE"): raise Exception() HAVE_SQLITE3 = True import sqlite3 assert hasattr(sqlite3, "connect") # graalpy except: HAVE_SQLITE3 = False try: import importlib.util HAVE_ZMQ = bool(importlib.util.find_spec("zmq")) except: HAVE_ZMQ = False try: if os.environ.get("PRTY_NO_PSUTIL"): raise Exception() HAVE_PSUTIL = True import psutil except: HAVE_PSUTIL = False try: if os.environ.get("PRTY_NO_MAGIC") or ( ANYWIN and not os.environ.get("PRTY_FORCE_MAGIC") ): raise Exception() import magic except: pass if os.environ.get("PRTY_MODSPEC"): from inspect import getsourcefile print("PRTY_MODSPEC: ifaddr:", getsourcefile(get_adapters)) if True: # pylint: disable=using-constant-test import types from collections.abc import Callable, Iterable import typing from typing import IO, Any, Generator, Optional, Pattern, Protocol, Union try: from typing import LiteralString except: pass class RootLogger(Protocol): def __call__(self, src: str, msg: str, c: Union[int, str] = 0) -> None: return None class NamedLogger(Protocol): def __call__(self, msg: str, c: Union[int, str] = 0) -> None: return None if TYPE_CHECKING: from .authsrv import VFS from .broker_util import BrokerCli from .up2k import Up2k FAKE_MP = False try: if os.environ.get("PRTY_NO_MP"): raise ImportError() import multiprocessing as mp # import multiprocessing.dummy as mp except ImportError: # support jython mp = None # type: ignore if not PY2: from io import BytesIO else: from StringIO import StringIO as BytesIO # type: ignore try: if os.environ.get("PRTY_NO_IPV6"): raise Exception() socket.inet_pton(socket.AF_INET6, "::1") HAVE_IPV6 = True if GRAAL: try: # --python.PosixModuleBackend=java throws OSError: illegal IP address socket.inet_pton(socket.AF_INET, "127.0.0.1") except: _inet_pton = socket.inet_pton def inet_pton(fam, ip): if fam == socket.AF_INET: return socket.inet_aton(ip) return _inet_pton(fam, ip) socket.inet_pton = inet_pton except: def inet_pton(fam, ip): return socket.inet_aton(ip) socket.inet_pton = inet_pton HAVE_IPV6 = False try: struct.unpack(b">i", b"idgi") spack = struct.pack # type: ignore sunpack = struct.unpack # type: ignore except: def spack(fmt: bytes, *a: Any) -> bytes: return struct.pack(fmt.decode("ascii"), *a) def sunpack(fmt: bytes, a: bytes) -> tuple[Any, ...]: return struct.unpack(fmt.decode("ascii"), a) try: BITNESS = struct.calcsize(b"P") * 8 except: BITNESS = struct.calcsize("P") * 8 CAN_SIGMASK = not (ANYWIN or PY2 or GRAAL) RE_ANSI = re.compile("\033\\[[^mK]*[mK]") RE_HTML_SH = re.compile(r"[<>&$?`\"';]") RE_CTYPE = re.compile(r"^content-type: *([^; ]+)", re.IGNORECASE) RE_CDISP = re.compile(r"^content-disposition: *([^; ]+)", re.IGNORECASE) RE_CDISP_FIELD = re.compile( r'^content-disposition:(?: *|.*; *)name="([^"]+)"', re.IGNORECASE ) RE_CDISP_FILE = re.compile( r'^content-disposition:(?: *|.*; *)filename="(.*)"', re.IGNORECASE ) RE_MEMTOTAL = re.compile("^MemTotal:.* kB") RE_MEMAVAIL = re.compile("^MemAvailable:.* kB") if PY2: def umktrans(s1, s2): return {ord(c1): ord(c2) for c1, c2 in zip(s1, s2)} else: umktrans = str.maketrans FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/") VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\") APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/") FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":") FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None BOS_SEP = ("%s" % (os.sep,)).encode("ascii") if WINDOWS and PY2: FS_ENCODING = "utf-8" else: FS_ENCODING = sys.getfilesystemencoding() SYMTIME = PY36 and os.utime in os.supports_follow_symlinks META_NOBOTS = '\n' # smart enough to understand javascript while also ignoring rel="nofollow" BAD_BOTS = r"Barkrowler|bingbot|BLEXBot|Googlebot|GoogleOther|GPTBot|PetalBot|SeekportBot|SemrushBot|YandexBot" FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z" URL_PRJ = "https://github.com/9001/copyparty" URL_BUG = URL_PRJ + "/issues/new?labels=bug&template=bug_report.md" HTTPCODE = { 200: "OK", 201: "Created", 202: "Accepted", 204: "No Content", 206: "Partial Content", 207: "Multi-Status", 301: "Moved Permanently", 302: "Found", 304: "Not Modified", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 409: "Conflict", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 415: "Unsupported Media Type", 416: "Requested Range Not Satisfiable", 422: "Unprocessable Entity", 423: "Locked", 429: "Too Many Requests", 500: "Internal Server Error", 501: "Not Implemented", 503: "Service Unavailable", 999: "MissingNo", } IMPLICATIONS = [ ["e2dsa", "e2ds"], ["e2ds", "e2d"], ["e2tsr", "e2ts"], ["e2ts", "e2t"], ["e2t", "e2d"], ["e2vu", "e2v"], ["e2vp", "e2v"], ["e2v", "e2d"], ["hardlink_only", "hardlink"], ["hardlink", "dedup"], ["tftpvv", "tftpv"], ["nodupem", "nodupe"], ["no_dupe_m", "no_dupe"], ["nohtml", "noscript"], ["sftpvv", "sftpv"], ["smbw", "smb"], ["smb1", "smb"], ["smbvvv", "smbvv"], ["smbvv", "smbv"], ["smbv", "smb"], ["zv", "zmv"], ["zv", "zsv"], ["z", "zm"], ["z", "zs"], ["zmvv", "zmv"], ["zm4", "zm"], ["zm6", "zm"], ["zmv", "zm"], ["zms", "zm"], ["zsv", "zs"], ] if ANYWIN: IMPLICATIONS.extend([["z", "zm4"]]) UNPLICATIONS = [["no_dav", "daw"]] DAV_ALLPROP_L = [ "contentclass", "creationdate", "defaultdocument", "displayname", "getcontentlanguage", "getcontentlength", "getcontenttype", "getlastmodified", "href", "iscollection", "ishidden", "isreadonly", "isroot", "isstructureddocument", "lastaccessed", "name", "parentname", "resourcetype", "supportedlock", ] DAV_ALLPROPS = set(DAV_ALLPROP_L) FAVICON_MIMES = { "gif": "image/gif", "png": "image/png", "svg": "image/svg+xml", } MIMES = { "opus": "audio/ogg; codecs=opus", "owa": "audio/webm; codecs=opus", } def _add_mimes() -> set[str]: # `mimetypes` is woefully unpopulated on windows # but will be used as fallback on linux for ln in """text css html csv application json wasm xml pdf rtf zip jar fits wasm image webp jpeg png gif bmp jxl jp2 jxs jxr tiff bpg heic heif avif audio aac ogg wav flac ape amr video webm mp4 mpeg font woff woff2 otf ttf """.splitlines(): k, vs = ln.split(" ", 1) for v in vs.strip().split(): MIMES[v] = "{}/{}".format(k, v) for ln in """text md=plain txt=plain js=javascript application 7z=x-7z-compressed tar=x-tar bz2=x-bzip2 gz=gzip rar=x-rar-compressed zst=zstd xz=x-xz lz=lzip cpio=x-cpio application msi=x-ms-installer cab=vnd.ms-cab-compressed rpm=x-rpm crx=x-chrome-extension application epub=epub+zip mobi=x-mobipocket-ebook lit=x-ms-reader rss=rss+xml atom=atom+xml torrent=x-bittorrent application p7s=pkcs7-signature dcm=dicom shx=vnd.shx shp=vnd.shp dbf=x-dbf gml=gml+xml gpx=gpx+xml amf=x-amf application swf=x-shockwave-flash m3u=vnd.apple.mpegurl db3=vnd.sqlite3 sqlite=vnd.sqlite3 text ass=plain ssa=plain image jpg=jpeg xpm=x-xpixmap psd=vnd.adobe.photoshop jpf=jpx tif=tiff ico=x-icon djvu=vnd.djvu image heics=heic-sequence heifs=heif-sequence hdr=vnd.radiance svg=svg+xml image arw=x-sony-arw cr2=x-canon-cr2 crw=x-canon-crw dcr=x-kodak-dcr dng=x-adobe-dng erf=x-epson-erf image k25=x-kodak-k25 kdc=x-kodak-kdc mrw=x-minolta-mrw nef=x-nikon-nef orf=x-olympus-orf image pef=x-pentax-pef raf=x-fuji-raf raw=x-panasonic-raw sr2=x-sony-sr2 srf=x-sony-srf x3f=x-sigma-x3f audio caf=x-caf mp3=mpeg m4a=mp4 m4b=mp4 m4r=mp4 mid=midi mpc=musepack aif=aiff au=basic qcp=qcelp video mkv=x-matroska mov=quicktime avi=x-msvideo m4v=x-m4v ts=mp2t video asf=x-ms-asf flv=x-flv 3gp=3gpp 3g2=3gpp2 rmvb=vnd.rn-realmedia-vbr font ttc=collection """.splitlines(): k, ems = ln.split(" ", 1) for em in ems.strip().split(): ext, mime = em.split("=") MIMES[ext] = "{}/{}".format(k, mime) ptn = re.compile("html|script|tension|wasm|xml") return {x for x in MIMES.values() if not ptn.search(x)} SAFE_MIMES = _add_mimes() EXTS: dict[str, str] = {v: k for k, v in MIMES.items()} EXTS["vnd.mozilla.apng"] = "png" MAGIC_MAP = {"jpeg": "jpg"} DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime" DEF_MTE = ".files,circle,album,.tn,artist,title,tdate,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash" DEF_MTH = "tdate,.vq,.aq,vc,ac,fmt,res,.fps" REKOBO_KEY = { v: ln.split(" ", 1)[0] for ln in """ 1B 6d B 2B 7d Gb F# 3B 8d Db C# 4B 9d Ab G# 5B 10d Eb D# 6B 11d Bb A# 7B 12d F 8B 1d C 9B 2d G 10B 3d D 11B 4d A 12B 5d E 1A 6m Abm G#m 2A 7m Ebm D#m 3A 8m Bbm A#m 4A 9m Fm 5A 10m Cm 6A 11m Gm 7A 12m Dm 8A 1m Am 9A 2m Em 10A 3m Bm 11A 4m Gbm F#m 12A 5m Dbm C#m """.strip().split( "\n" ) for v in ln.strip().split(" ")[1:] if v } REKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()} _exestr = "python3 python ffmpeg ffprobe cfssl cfssljson cfssl-certinfo" CMD_EXEB = set(_exestr.encode("utf-8").split()) CMD_EXES = set(_exestr.split()) # mostly from https://github.com/github/gitignore/blob/main/Global/macOS.gitignore APPLESAN_TXT = r"/(__MACOS|Icon\r\r)|/\.(_|DS_Store|AppleDouble|LSOverride|DocumentRevisions-|fseventsd|Spotlight-|TemporaryItems|Trashes|VolumeIcon\.icns|com\.apple\.timemachine\.donotpresent|AppleDB|AppleDesktop|apdisk)" APPLESAN_RE = re.compile(APPLESAN_TXT) HUMANSIZE_UNITS = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB") UNHUMANIZE_UNITS = { "b": 1, "k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024, "t": 1024 * 1024 * 1024 * 1024, "p": 1024 * 1024 * 1024 * 1024 * 1024, "e": 1024 * 1024 * 1024 * 1024 * 1024 * 1024, } VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} FN_EMB = set([".prologue.html", ".epilogue.html", "readme.md", "preadme.md"]) def read_ram() -> tuple[float, float]: # NOTE: apparently no need to consider /sys/fs/cgroup/memory.max # (cgroups2) since the limit is synced to /proc/meminfo a = b = 0 try: with open("/proc/meminfo", "rb", 0x10000) as f: zsl = f.read(0x10000).decode("ascii", "replace").split("\n") 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_MEMAVAIL zs = next((x for x in zsl if p.match(x))) b = int((int(zs.split()[1]) / 0x100000) * 100) / 100 except: pass return a, b RAM_TOTAL, RAM_AVAIL = read_ram() pybin = sys.executable or "" if EXE: pybin = "" for zsg in "python3 python".split(): try: if ANYWIN: zsg += ".exe" zsg = shutil.which(zsg) if zsg: pybin = zsg break except: pass def py_desc() -> str: interp = platform.python_implementation() py_ver = ".".join([str(x) for x in sys.version_info]) ofs = py_ver.find(".final.") if ofs > 0: py_ver = py_ver[:ofs] if "free-threading" in sys.version: py_ver += "t" host_os = platform.system() compiler = platform.python_compiler().split("http")[0] m = re.search(r"([0-9]+\.[0-9\.]+)", platform.version()) os_ver = m.group(1) if m else "" return "{:>9} v{} on {}{} {} [{}]".format( interp, py_ver, host_os, BITNESS, os_ver, compiler ) def expat_ver() -> str: try: import pyexpat return ".".join([str(x) for x in pyexpat.version_info]) except: return "?" def _sqlite_ver() -> str: assert sqlite3 # type: ignore # !rm try: co = sqlite3.connect(":memory:") cur = co.cursor() try: vs = cur.execute("select * from pragma_compile_options").fetchall() except: vs = cur.execute("pragma compile_options").fetchall() v = next(x[0].split("=")[1] for x in vs if x[0].startswith("THREADSAFE=")) cur.close() co.close() except: v = "W" return "{}*{}".format(sqlite3.sqlite_version, v) try: SQLITE_VER = _sqlite_ver() except: SQLITE_VER = "(None)" try: from jinja2 import __version__ as JINJA_VER except: JINJA_VER = "(None)" try: if os.environ.get("PRTY_NO_PYFTPD"): raise Exception() from pyftpdlib.__init__ import __ver__ as PYFTPD_VER except: PYFTPD_VER = "(None)" try: if os.environ.get("PRTY_NO_PARTFTPY"): raise Exception() from partftpy.__init__ import __version__ as PARTFTPY_VER except: PARTFTPY_VER = "(None)" try: if os.environ.get("PRTY_NO_PARAMIKO"): raise Exception() from paramiko import __version__ as MIKO_VER except: MIKO_VER = "(None)" PY_DESC = py_desc() VERSIONS = "copyparty v{} ({})\n{}\n sqlite {} | jinja {} | pyftpd {} | tftp {} | miko {}".format( S_VERSION, S_BUILD_DT, PY_DESC, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER, MIKO_VER, ) try: _b64_enc_tl = bytes.maketrans(b"+/", b"-_") _b64_dec_tl = bytes.maketrans(b"-_", b"+/") def ub64enc(bs: bytes) -> bytes: x = binascii.b2a_base64(bs, newline=False) return x.translate(_b64_enc_tl) def ub64dec(bs: bytes) -> bytes: bs = bs.translate(_b64_dec_tl) return binascii.a2b_base64(bs) def b64enc(bs: bytes) -> bytes: return binascii.b2a_base64(bs, newline=False) def b64dec(bs: bytes) -> bytes: return binascii.a2b_base64(bs) zb = b">>>????" zb2 = base64.urlsafe_b64encode(zb) if zb2 != ub64enc(zb) or zb != ub64dec(zb2): raise Exception("bad smoke") except Exception as ex: ub64enc = base64.urlsafe_b64encode # type: ignore ub64dec = base64.urlsafe_b64decode # type: ignore b64enc = base64.b64encode # type: ignore b64dec = base64.b64decode # type: ignore if PY36: print("using fallback base64 codec due to %r" % (ex,)) class NotUTF8(Exception): pass def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool) -> str: with open(ap, "rb") as f: buf = f.read() if buf.startswith(b"\xef\xbb\xbf"): buf = buf[3:] try: return buf.decode("utf-8", "strict") except UnicodeDecodeError as ex: eo = ex.start eb = buf[eo : eo + 1] if not strict: t = "WARNING: The file [%s] is not using the UTF-8 character encoding; some characters in the file will be skipped/ignored. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8." t = t % (ap, eb, eo) if log: log(t, 3) else: print(t) return buf.decode("utf-8", "replace") t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8." t = t % (ap, eb, eo) if log: log(t, 3) else: print(t) raise NotUTF8(t) class Daemon(threading.Thread): def __init__( self, target: Any, name: Optional[str] = None, a: Optional[Iterable[Any]] = None, r: bool = True, ka: Optional[dict[Any, Any]] = None, ) -> None: threading.Thread.__init__(self, name=name) self.a = a or () self.ka = ka or {} self.fun = target self.daemon = True if r: self.start() def run(self): if CAN_SIGMASK: signal.pthread_sigmask( signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1] ) self.fun(*self.a, **self.ka) class Netdev(object): def __init__(self, ip: str, idx: int, name: str, desc: str): self.ip = ip self.idx = idx self.name = name self.desc = desc def __str__(self): return "{}-{}{}".format(self.idx, self.name, self.desc) def __repr__(self): return "'{}-{}'".format(self.idx, self.name) def __lt__(self, rhs): return str(self) < str(rhs) def __eq__(self, rhs): return str(self) == str(rhs) class Cooldown(object): def __init__(self, maxage: float) -> None: self.maxage = maxage self.mutex = threading.Lock() self.hist: dict[str, float] = {} self.oldest = 0.0 def poke(self, key: str) -> bool: with self.mutex: now = time.time() ret = False pv: float = self.hist.get(key, 0) if now - pv > self.maxage: self.hist[key] = now ret = True if self.oldest - now > self.maxage * 2: self.hist = { k: v for k, v in self.hist.items() if now - v < self.maxage } self.oldest = sorted(self.hist.values())[0] return ret class HLog(logging.Handler): def __init__(self, log_func: "RootLogger") -> None: logging.Handler.__init__(self) self.log_func = log_func self.ptn_ftp = re.compile(r"^([0-9a-f:\.]+:[0-9]{1,5})-\[") self.ptn_smb_ign = re.compile(r"^(Callback added|Config file parsed)") def __repr__(self) -> str: level = logging.getLevelName(self.level) return "<%s cpp(%s)>" % (self.__class__.__name__, level) def flush(self) -> None: pass def emit(self, record: logging.LogRecord) -> None: msg = self.format(record) lv = record.levelno if lv < logging.INFO: c = 6 elif lv < logging.WARNING: c = 0 elif lv < logging.ERROR: c = 3 else: c = 1 if record.name == "pyftpdlib": m = self.ptn_ftp.match(msg) if m: ip = m.group(1) msg = msg[len(ip) + 1 :] if ip.startswith("::ffff:"): record.name = ip[7:] else: record.name = ip elif record.name.startswith("impacket"): if self.ptn_smb_ign.match(msg): return elif record.name.startswith("partftpy."): record.name = record.name[9:] self.log_func(record.name[-21:], msg, c) class NetMap(object): def __init__( self, ips: list[str], cidrs: list[str], keep_lo=False, strict_cidr=False, defer_mutex=False, ) -> None: """ ips: list of plain ipv4/ipv6 IPs, not cidr cidrs: list of cidr-notation IPs (ip/prefix) """ # fails multiprocessing; defer assignment self.mutex: Optional[threading.Lock] = None if defer_mutex else threading.Lock() if "::" in ips: ips = [x for x in ips if x != "::"] + list( [x.split("/")[0] for x in cidrs if ":" in x] ) ips.append("0.0.0.0") if "0.0.0.0" in ips: ips = [x for x in ips if x != "0.0.0.0"] + list( [x.split("/")[0] for x in cidrs if ":" not in x] ) if not keep_lo: ips = [x for x in ips if x not in ("::1", "127.0.0.1")] ips = find_prefix(ips, cidrs) self.cache: dict[str, str] = {} self.b2sip: dict[bytes, str] = {} self.b2net: dict[bytes, Union[IPv4Network, IPv6Network]] = {} self.bip: list[bytes] = [] for ip in ips: v6 = ":" in ip fam = socket.AF_INET6 if v6 else socket.AF_INET bip = socket.inet_pton(fam, ip.split("/")[0]) self.bip.append(bip) self.b2sip[bip] = ip.split("/")[0] self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, strict_cidr) self.bip.sort(reverse=True) def map(self, ip: str) -> str: if ip.startswith("::ffff:"): ip = ip[7:] try: return self.cache[ip] except: # intentionally crash the calling thread if unset: assert self.mutex # type: ignore # !rm with self.mutex: return self._map(ip) def _map(self, ip: str) -> str: v6 = ":" in ip ci = IPv6Address(ip) if v6 else IPv4Address(ip) bip = next((x for x in self.bip if ci in self.b2net[x]), None) ret = self.b2sip[bip] if bip else "" if len(self.cache) > 9000: self.cache = {} self.cache[ip] = ret return ret class UnrecvEOF(OSError): pass class _Unrecv(object): """ undo any number of socket recv ops """ def __init__(self, s: socket.socket, log: Optional["NamedLogger"]) -> None: self.s = s self.log = log self.buf: bytes = b"" self.nb = 0 self.te = 0 def recv(self, nbytes: int, spins: int = 1) -> bytes: if self.buf: ret = self.buf[:nbytes] self.buf = self.buf[nbytes:] self.nb += len(ret) return ret while True: try: ret = self.s.recv(nbytes) break except socket.timeout: spins -= 1 if spins <= 0: ret = b"" break continue except: ret = b"" break if not ret: raise UnrecvEOF("client stopped sending data") self.nb += len(ret) return ret def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes: """read an exact number of bytes""" ret = b"" try: while nbytes > len(ret): ret += self.recv(nbytes - len(ret)) except OSError: t = "client stopped sending data; expected at least %d more bytes" if not ret: t = t % (nbytes,) else: t += ", only got %d" t = t % (nbytes, len(ret)) if len(ret) <= 16: t += "; %r" % (ret,) if raise_on_trunc: raise UnrecvEOF(5, t) elif self.log: self.log(t, 3) return ret def unrecv(self, buf: bytes) -> None: self.buf = buf + self.buf self.nb -= len(buf) # !rm.yes> class _LUnrecv(object): """ with expensive debug logging """ def __init__(self, s: socket.socket, log: Optional["NamedLogger"]) -> None: self.s = s self.log = log self.buf = b"" self.nb = 0 def recv(self, nbytes: int, spins: int) -> bytes: if self.buf: ret = self.buf[:nbytes] self.buf = self.buf[nbytes:] t = "\033[0;7mur:pop:\033[0;1;32m {}\n\033[0;7mur:rem:\033[0;1;35m {}\033[0m" print(t.format(ret, self.buf)) self.nb += len(ret) return ret ret = self.s.recv(nbytes) t = "\033[0;7mur:recv\033[0;1;33m {}\033[0m" print(t.format(ret)) if not ret: raise UnrecvEOF("client stopped sending data") self.nb += len(ret) return ret def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes: """read an exact number of bytes""" try: ret = self.recv(nbytes, 1) err = False except: ret = b"" err = True while not err and len(ret) < nbytes: try: ret += self.recv(nbytes - len(ret), 1) except OSError: err = True if err: t = "client only sent {} of {} expected bytes".format(len(ret), nbytes) if raise_on_trunc: raise UnrecvEOF(t) elif self.log: self.log(t, 3) return ret def unrecv(self, buf: bytes) -> None: self.buf = buf + self.buf self.nb -= len(buf) t = "\033[0;7mur:push\033[0;1;31m {}\n\033[0;7mur:rem:\033[0;1;35m {}\033[0m" print(t.format(buf, self.buf)) # !rm.no> Unrecv = _Unrecv class CachedSet(object): def __init__(self, maxage: float) -> None: self.c: dict[Any, float] = {} self.maxage = maxage self.oldest = 0.0 def add(self, v: Any) -> None: self.c[v] = time.time() def cln(self) -> None: now = time.time() if now - self.oldest < self.maxage: return c = self.c = {k: v for k, v in self.c.items() if now - v < self.maxage} try: self.oldest = c[min(c, key=c.get)] # type: ignore except: self.oldest = now class CachedDict(object): def __init__(self, maxage: float) -> None: self.c: dict[str, tuple[float, Any]] = {} self.maxage = maxage self.oldest = 0.0 def set(self, k: str, v: Any) -> None: now = time.time() self.c[k] = (now, v) if now - self.oldest < self.maxage: return c = self.c = {k: v for k, v in self.c.items() if now - v[0] < self.maxage} try: self.oldest = min([x[0] for x in c.values()]) except: self.oldest = now def get(self, k: str) -> Optional[tuple[str, Any]]: try: ts, ret = self.c[k] now = time.time() if now - ts > self.maxage: del self.c[k] return None return ret except: return None class FHC(object): class CE(object): def __init__(self, fh: typing.BinaryIO) -> None: self.ts: float = 0 self.fhs = [fh] self.all_fhs = set([fh]) def __init__(self) -> None: self.cache: dict[str, FHC.CE] = {} self.aps: dict[str, int] = {} def close(self, path: str) -> None: try: ce = self.cache[path] except: return for fh in ce.fhs: fh.close() del self.cache[path] del self.aps[path] def clean(self) -> None: if not self.cache: return keep = {} now = time.time() for path, ce in self.cache.items(): if now < ce.ts + 5: keep[path] = ce else: for fh in ce.fhs: fh.close() self.cache = keep def pop(self, path: str) -> typing.BinaryIO: return self.cache[path].fhs.pop() def put(self, path: str, fh: typing.BinaryIO) -> None: if path not in self.aps: self.aps[path] = 0 try: ce = self.cache[path] ce.all_fhs.add(fh) ce.fhs.append(fh) except: ce = self.CE(fh) self.cache[path] = ce ce.ts = time.time() class ProgressPrinter(threading.Thread): """ periodically print progress info without linefeeds """ def __init__(self, log: "NamedLogger", args: argparse.Namespace) -> None: threading.Thread.__init__(self, name="pp") self.daemon = True self.log = log self.args = args self.msg = "" self.end = False self.n = -1 def run(self) -> None: sigblock() tp = 0 msg = None slp_pr = self.args.scan_pr_r slp_ps = min(slp_pr, self.args.scan_st_r) no_stdout = self.args.q or slp_pr == slp_ps fmt = " {}\033[K\r" if VT100 else " {} $\r" while not self.end: time.sleep(slp_ps) if msg == self.msg or self.end: continue msg = self.msg now = time.time() if msg and now - tp >= slp_pr: tp = now self.log("progress: %r" % (msg,), 6) if no_stdout: continue uprint(fmt.format(msg)) if PY2: sys.stdout.flush() if no_stdout: return if VT100: print("\033[K", end="") elif msg: print("------------------------") sys.stdout.flush() # necessary on win10 even w/ stderr btw class MTHash(object): def __init__(self, cores: int): self.pp: Optional[ProgressPrinter] = None self.f: Optional[typing.BinaryIO] = None self.sz = 0 self.csz = 0 self.stop = False self.readsz = 1024 * 1024 * (2 if (RAM_AVAIL or 2) < 1 else 12) self.omutex = threading.Lock() self.imutex = threading.Lock() self.work_q: Queue[int] = Queue() self.done_q: Queue[tuple[int, str, int, int]] = Queue() self.thrs = [] for n in range(cores): t = Daemon(self.worker, "mth-" + str(n)) self.thrs.append(t) def hash( self, f: typing.BinaryIO, fsz: int, chunksz: int, pp: Optional[ProgressPrinter] = None, prefix: str = "", suffix: str = "", ) -> list[tuple[str, int, int]]: with self.omutex: self.f = f self.sz = fsz self.csz = chunksz chunks: dict[int, tuple[str, int, int]] = {} nchunks = int(math.ceil(fsz / chunksz)) for nch in range(nchunks): self.work_q.put(nch) ex: Optional[Exception] = None for nch in range(nchunks): qe = self.done_q.get() try: nch, dig, ofs, csz = qe chunks[nch] = (dig, ofs, csz) except: ex = ex or qe # type: ignore if pp: mb = (fsz - nch * chunksz) // (1024 * 1024) pp.msg = prefix + str(mb) + suffix if ex: raise ex ret = [] for n in range(nchunks): ret.append(chunks[n]) self.f = None self.csz = 0 self.sz = 0 return ret def worker(self) -> None: while True: ofs = self.work_q.get() try: v = self.hash_at(ofs) except Exception as ex: v = ex # type: ignore self.done_q.put(v) def hash_at(self, nch: int) -> tuple[int, str, int, int]: f = self.f ofs = ofs0 = nch * self.csz chunk_sz = chunk_rem = min(self.csz, self.sz - ofs) if self.stop: return nch, "", ofs0, chunk_sz assert f # !rm hashobj = hashlib.sha512() while chunk_rem > 0: with self.imutex: f.seek(ofs) buf = f.read(min(chunk_rem, self.readsz)) if not buf: raise Exception("EOF at " + str(ofs)) hashobj.update(buf) chunk_rem -= len(buf) ofs += len(buf) bdig = hashobj.digest()[:33] udig = ub64enc(bdig).decode("ascii") return nch, udig, ofs0, chunk_sz class HMaccas(object): def __init__(self, keypath: str, retlen: int) -> None: self.retlen = retlen self.cache: dict[bytes, str] = {} try: with open(keypath, "rb") as f: self.key = f.read() if len(self.key) != 64: raise Exception() except: self.key = os.urandom(64) with open(keypath, "wb") as f: f.write(self.key) def b(self, msg: bytes) -> str: try: return self.cache[msg] except: if len(self.cache) > 9000: self.cache = {} zb = hmac.new(self.key, msg, hashlib.sha512).digest() zs = ub64enc(zb)[: self.retlen].decode("ascii") self.cache[msg] = zs return zs def s(self, msg: str) -> str: return self.b(msg.encode("utf-8", "replace")) class Magician(object): def __init__(self) -> None: self.bad_magic = False self.mutex = threading.Lock() self.magic: Optional["magic.Magic"] = None def ext(self, fpath: str) -> str: try: if self.bad_magic: raise Exception() if not self.magic: try: with self.mutex: if not self.magic: self.magic = magic.Magic(uncompress=False, extension=True) except: self.bad_magic = True raise with self.mutex: ret = self.magic.from_file(fpath) except: ret = "?" ret = ret.split("/")[0] ret = MAGIC_MAP.get(ret, ret) if "?" not in ret: return ret mime = magic.from_file(fpath, mime=True) mime = re.split("[; ]", mime, maxsplit=1)[0] try: return EXTS[mime] except: pass mg = mimetypes.guess_extension(mime) if mg: return mg[1:] else: raise Exception() class Garda(object): """ban clients for repeated offenses""" def __init__(self, cfg: str, uniq: bool = True) -> None: self.uniq = uniq try: a, b, c = cfg.strip().split(",") self.lim = int(a) self.win = int(b) * 60 self.pen = int(c) * 60 except: self.lim = self.win = self.pen = 0 self.ct: dict[str, list[int]] = {} self.prev: dict[str, str] = {} self.last_cln = 0 def cln(self, ip: str) -> None: n = 0 ok = int(time.time() - self.win) for v in self.ct[ip]: if v < ok: n += 1 else: break if n: te = self.ct[ip][n:] if te: self.ct[ip] = te else: del self.ct[ip] try: del self.prev[ip] except: pass def allcln(self) -> None: for k in list(self.ct): self.cln(k) self.last_cln = int(time.time()) def bonk(self, ip: str, prev: str) -> tuple[int, str]: if not self.lim: return 0, ip if ":" in ip: # assume /64 clients; drop 4 groups ip = IPv6Address(ip).exploded[:-20] if prev and self.uniq: if self.prev.get(ip) == prev: return 0, ip self.prev[ip] = prev now = int(time.time()) try: self.ct[ip].append(now) except: self.ct[ip] = [now] if now - self.last_cln > 300: self.allcln() else: self.cln(ip) if len(self.ct[ip]) >= self.lim: return now + self.pen, ip else: return 0, ip if WINDOWS and sys.version_info < (3, 8): _popen = sp.Popen def _spopen(c, *a, **ka): enc = sys.getfilesystemencoding() c = [x.decode(enc, "replace") if hasattr(x, "decode") else x for x in c] return _popen(c, *a, **ka) sp.Popen = _spopen def uprint(msg: str) -> None: try: print(msg, end="") except UnicodeEncodeError: try: print(msg.encode("utf-8", "replace").decode(), end="") except: print(msg.encode("ascii", "replace").decode(), end="") def nuprint(msg: str) -> None: uprint("%s\n" % (msg,)) def dedent(txt: str) -> str: pad = 64 lns = txt.replace("\r", "").split("\n") for ln in lns: zs = ln.lstrip() pad2 = len(ln) - len(zs) if zs and pad > pad2: pad = pad2 return "\n".join([ln[pad:] for ln in lns]) def rice_tid() -> str: tid = threading.current_thread().ident c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) return "".join("\033[1;37;48;5;{0}m{0:02x}".format(x) for x in c) + "\033[0m" def trace(*args: Any, **kwargs: Any) -> None: t = time.time() stack = "".join( "\033[36m%s\033[33m%s" % (x[0].split(os.sep)[-1][:-3], x[1]) for x in traceback.extract_stack()[3:-1] ) parts = ["%.6f" % (t,), rice_tid(), stack] if args: parts.append(repr(args)) if kwargs: parts.append(repr(kwargs)) msg = "\033[0m ".join(parts) # _tracebuf.append(msg) nuprint(msg) def alltrace(verbose: bool = True) -> str: threads: dict[str, types.FrameType] = {} names = dict([(t.ident, t.name) for t in threading.enumerate()]) for tid, stack in sys._current_frames().items(): if verbose: name = "%s (%x)" % (names.get(tid), tid) else: name = str(names.get(tid)) threads[name] = stack rret: list[str] = [] bret: list[str] = [] np = -3 if verbose else -2 for name, stack in sorted(threads.items()): ret = ["\n\n# %s" % (name,)] pad = None for fn, lno, name, line in traceback.extract_stack(stack): fn = os.sep.join(fn.split(os.sep)[np:]) ret.append('File: "%s", line %d, in %s' % (fn, lno, name)) if line: ret.append(" " + str(line.strip())) if "self.not_empty.wait()" in line: pad = " " * 4 if pad: bret += [ret[0]] + [pad + x for x in ret[1:]] else: rret.extend(ret) return "\n".join(rret + bret) + "\n" def start_stackmon(arg_str: str, nid: int) -> None: suffix = "-{}".format(nid) if nid else "" fp, f = arg_str.rsplit(",", 1) zi = int(f) Daemon(stackmon, "stackmon" + suffix, (fp, zi, suffix)) def stackmon(fp: str, ival: float, suffix: str) -> None: ctr = 0 fp0 = fp while True: ctr += 1 fp = fp0 time.sleep(ival) st = "{}, {}\n{}".format(ctr, time.time(), alltrace()) buf = st.encode("utf-8", "replace") if fp.endswith(".gz"): # 2459b 2304b 2241b 2202b 2194b 2191b lv3..8 # 0.06s 0.08s 0.11s 0.13s 0.16s 0.19s buf = gzip.compress(buf, compresslevel=6) elif fp.endswith(".xz"): import lzma # 2276b 2216b 2200b 2192b 2168b lv0..4 # 0.04s 0.10s 0.22s 0.41s 0.70s buf = lzma.compress(buf, preset=0) if "%" in fp: dt = datetime.now(UTC) for fs in "YmdHMS": fs = "%" + fs if fs in fp: fp = fp.replace(fs, dt.strftime(fs)) if "/" in fp: try: os.makedirs(fp.rsplit("/", 1)[0]) except: pass with open(fp + suffix, "wb") as f: f.write(buf) def start_log_thrs( logger: Callable[[str, str, int], None], ival: float, nid: int ) -> None: ival = float(ival) tname = lname = "log-thrs" if nid: tname = "logthr-n{}-i{:x}".format(nid, os.getpid()) lname = tname[3:] Daemon(log_thrs, tname, (logger, ival, lname)) def log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> None: while True: time.sleep(ival) tv = [x.name for x in threading.enumerate()] tv = [ x.split("-")[0] if x.split("-")[0] in ["httpconn", "thumb", "tagger"] else "listen" if "-listen-" in x else x for x in tv if not x.startswith("pydevd.") ] tv = ["{}\033[36m{}".format(v, k) for k, v in sorted(Counter(tv).items())] log(name, "\033[0m \033[33m".join(tv), 3) def _sigblock(): signal.pthread_sigmask( signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1] ) sigblock = _sigblock if CAN_SIGMASK else noop def vol_san(vols: list["VFS"], txt: bytes) -> bytes: txt0 = txt for vol in vols: bap = vol.realpath.encode("utf-8") bhp = vol.histpath.encode("utf-8") bvp = vol.vpath.encode("utf-8") bvph = b"$hist(/" + bvp + b")" if bap: txt = txt.replace(bap, bvp) txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp) if bhp: txt = txt.replace(bhp, bvph) txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph) if vol.histpath != vol.dbpath: bdp = vol.dbpath.encode("utf-8") bdph = b"$db(/" + bvp + b")" txt = txt.replace(bdp, bdph) txt = txt.replace(bdp.replace(b"\\", b"\\\\"), bdph) if txt != txt0: txt += b"\r\nNOTE: filepaths sanitized; see serverlog for correct values" return txt def min_ex(max_lines: int = 8, reverse: bool = False) -> str: et, ev, tb = sys.exc_info() stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1] fmt = "%s:%d <%s>: %s" ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb] if et or ev or tb: ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev)) return "\n".join(ex[-max_lines:][:: -1 if reverse else 1]) def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str]: fun = kwargs.pop("fun", open) fdir = kwargs.pop("fdir", None) suffix = kwargs.pop("suffix", None) vf = kwargs.pop("vf", None) fperms = vf and "fperms" in vf if fname == os.devnull: return fun(fname, *args, **kwargs), fname if suffix: ext = fname.split(".")[-1] if len(ext) < 7: suffix += "." + ext orig_name = fname bname = fname ext = "" while True: ofs = bname.rfind(".") if ofs < 0 or ofs < len(bname) - 7: # doesn't look like an extension anymore break ext = bname[ofs:] + ext bname = bname[:ofs] asciified = False b64 = "" while True: f = None try: if fdir: fpath = os.path.join(fdir, fname) else: fpath = fname if suffix and os.path.lexists(fsenc(fpath)): fpath += suffix fname += suffix ext += suffix f = fun(fsenc(fpath), *args, **kwargs) if b64: assert fdir # !rm fp2 = "fn-trunc.%s.txt" % (b64,) fp2 = os.path.join(fdir, fp2) with open(fsenc(fp2), "wb") as f2: f2.write(orig_name.encode("utf-8")) if fperms: set_fperms(f2, vf) if fperms: set_fperms(f, vf) return f, fname except OSError as ex_: ex = ex_ if f: f.close() # EPERM: android13 if ex.errno in (errno.EINVAL, errno.EPERM) and not asciified: asciified = True zsl = [] for zs in (bname, fname): zs = zs.encode("ascii", "replace").decode("ascii") zs = re.sub(r"[^][a-zA-Z0-9(){}.,+=!-]", "_", zs) zsl.append(zs) bname, fname = zsl continue # ENOTSUP: zfs on ubuntu 20.04 if ex.errno not in (errno.ENAMETOOLONG, errno.ENOSR, errno.ENOTSUP) and ( not WINDOWS or ex.errno != errno.EINVAL ): raise if not b64: zs = ("%s\n%s" % (orig_name, suffix)).encode("utf-8", "replace") b64 = ub64enc(hashlib.sha512(zs).digest()[:12]).decode("ascii") badlen = len(fname) while len(fname) >= badlen: if len(bname) < 8: raise ex if len(bname) > len(ext): # drop the last letter of the filename bname = bname[:-1] else: try: # drop the leftmost sub-extension _, ext = ext.split(".", 1) except: # okay do the first letter then ext = "." + ext[2:] fname = "%s~%s%s" % (bname, b64, ext) class MultipartParser(object): def __init__( self, log_func: "NamedLogger", args: argparse.Namespace, sr: Unrecv, http_headers: dict[str, str], ): self.sr = sr self.log = log_func self.args = args self.headers = http_headers try: self.clen = int(http_headers["content-length"]) sr.nb = 0 except: self.clen = 0 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[ Generator[ tuple[str, Optional[str], Generator[bytes, None, None]], None, None ] ] = None def _read_header(self) -> tuple[str, Optional[str]]: """ returns [fieldname, filename] after eating a block of multipart headers while doing a decent job at dealing with the absolute mess that is rfc1341/rfc1521/rfc2047/rfc2231/rfc2388/rfc6266/the-real-world (only the fallback non-js uploader relies on these filenames) """ for ln in read_header(self.sr, 2, 2592000): self.log(repr(ln)) m = self.re_ctype.match(ln) if m: if m.group(1).lower() == "multipart/mixed": # rfc-7578 overrides rfc-2388 so this is not-impl # (opera >=9 <11.10 is the only thing i've ever seen use it) raise Pebkac( 400, "you can't use that browser to upload multiple files at once", ) continue # the only other header we care about is content-disposition m = self.re_cdisp.match(ln) if not m: continue if m.group(1).lower() != "form-data": raise Pebkac(400, "not form-data: %r" % (ln,)) try: field = self.re_cdisp_field.match(ln).group(1) # type: ignore except: raise Pebkac(400, "missing field name: %r" % (ln,)) try: fn = self.re_cdisp_file.match(ln).group(1) # type: ignore except: # this is not a file upload, we're done return field, None try: is_webkit = "applewebkit" in self.headers["user-agent"].lower() except: is_webkit = False # chromes ignore the spec and makes this real easy if is_webkit: # quotes become %22 but they don't escape the % # so unescaping the quotes could turn messi return field, fn.split('"')[0] # also ez if filename doesn't contain " if not fn.split('"')[0].endswith("\\"): return field, fn.split('"')[0] # this breaks on firefox uploads that contain \" # since firefox escapes " but forgets to escape \ # so it'll truncate after the \ ret = "" esc = False for ch in fn: if esc: esc = False if ch not in ['"', "\\"]: ret += "\\" ret += ch elif ch == "\\": esc = True elif ch == '"': break else: ret += ch return field, ret raise Pebkac(400, "server expected a multipart header but you never sent one") def _read_data(self) -> Generator[bytes, None, None]: blen = len(self.boundary) bufsz = self.args.s_rd_sz while True: try: buf = self.sr.recv(bufsz) except: # abort: client disconnected raise Pebkac(400, "client d/c during multipart post") while True: ofs = buf.find(self.boundary) if ofs != -1: self.sr.unrecv(buf[ofs + blen :]) yield buf[:ofs] return d = len(buf) - blen if d > 0: # buffer growing large; yield everything except # the part at the end (maybe start of boundary) yield buf[:d] buf = buf[d:] # look for boundary near the end of the buffer n = 0 for n in range(1, len(buf) + 1): if not buf[-n:] in self.boundary: n -= 1 break if n == 0 or not self.boundary.startswith(buf[-n:]): # no boundary contents near the buffer edge break if blen == n: # EOF: found boundary yield buf[:-n] return try: buf += self.sr.recv(bufsz) except: # abort: client disconnected raise Pebkac(400, "client d/c during multipart post") yield buf def _run_gen( self, ) -> Generator[tuple[str, Optional[str], Generator[bytes, None, None]], None, None]: """ yields [fieldname, unsanitized_filename, fieldvalue] where fieldvalue yields chunks of data """ run = True while run: fieldname, filename = self._read_header() yield (fieldname, filename, self._read_data()) tail = self.sr.recv_ex(2, False) if tail == b"--": # EOF indicated by this immediately after final boundary if self.clen == self.sr.nb: tail = b"\r\n" # dillo doesn't terminate with trailing \r\n else: tail = self.sr.recv_ex(2, False) run = False if tail != b"\r\n": t = "protocol error after field value: want b'\\r\\n', got {!r}" raise Pebkac(400, t.format(tail)) def _read_value(self, iterable: Iterable[bytes], max_len: int) -> bytes: ret = b"" for buf in iterable: ret += buf if len(ret) > max_len: raise Pebkac(422, "field length is too long") return ret def parse(self) -> None: boundary = get_boundary(self.headers) if boundary.startswith('"') and boundary.endswith('"'): boundary = boundary[1:-1] # dillo uses quotes self.log("boundary=%r" % (boundary,)) # spec says there might be junk before the first boundary, # can't have the leading \r\n if that's not the case self.boundary = b"--" + boundary.encode("utf-8") # discard junk before the first boundary for junk in self._read_data(): if not junk: continue jtxt = junk.decode("utf-8", "replace") self.log("discarding preamble |%d| %r" % (len(junk), jtxt)) # nice, now make it fast self.boundary = b"\r\n" + self.boundary self.gen = self._run_gen() def require(self, field_name: str, max_len: int) -> str: """ returns the value of the next field in the multipart body, raises if the field name is not as expected """ assert self.gen # !rm p_field, p_fname, p_data = next(self.gen) if p_field != field_name: raise WrongPostKey(field_name, p_field, p_fname, p_data) return self._read_value(p_data, max_len).decode("utf-8", "surrogateescape") def drop(self) -> None: """discards the remaining multipart body""" assert self.gen # !rm for _, _, data in self.gen: for _ in data: pass def get_boundary(headers: dict[str, str]) -> str: # boundaries contain a-z A-Z 0-9 ' ( ) + _ , - . / : = ? # (whitespace allowed except as the last char) ptn = r"^multipart/form-data *; *(.*; *)?boundary=([^;]+)" ct = headers["content-type"] m = re.match(ptn, ct, re.IGNORECASE) if not m: raise Pebkac(400, "invalid content-type for a multipart post: %r" % (ct,)) return m.group(2) def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]: t0 = time.time() ret = b"" while True: if time.time() - t0 >= t_tot: return [] try: ret += sr.recv(1024, t_idle // 2) except: if not ret: return [] raise Pebkac( 400, "protocol error while reading headers", log=ret.decode("utf-8", "replace"), ) ofs = ret.find(b"\r\n\r\n") if ofs < 0: if len(ret) > 1024 * 32: raise Pebkac(400, "header 2big") else: continue if len(ret) > ofs + 4: sr.unrecv(ret[ofs + 4 :]) return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n") def rand_name(fdir: str, fn: str, rnd: int) -> str: ok = False try: ext = "." + fn.rsplit(".", 1)[1] except: ext = "" for extra in range(16): for _ in range(16): if ok: break nc = rnd + extra nb = (6 + 6 * nc) // 8 zb = ub64enc(os.urandom(nb)) fn = zb[:nc].decode("ascii") + ext ok = not os.path.exists(fsenc(os.path.join(fdir, fn))) return fn def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str: if alg == 1: zs = "%s %s %s %s" % (salt, fspath, fsize, inode) else: zs = "%s %s" % (salt, fspath) zb = zs.encode("utf-8", "replace") return ub64enc(hashlib.sha512(zb).digest()).decode("ascii") def _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str: return _gen_filekey(alg, salt, fspath.replace("/", "\\"), fsize, inode) gen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey def gen_filekey_dbg( alg: int, salt: str, fspath: str, fsize: int, inode: int, log: "NamedLogger", log_ptn: Optional[Pattern[str]], ) -> str: ret = gen_filekey(alg, salt, fspath, fsize, inode) assert log_ptn # !rm if log_ptn.search(fspath): try: import inspect ctx = ",".join(inspect.stack()[n].function for n in range(2, 5)) except: ctx = "" p2 = "a" try: p2 = absreal(fspath) if p2 != fspath: raise Exception() except: t = "maybe wrong abspath for filekey;\norig: %r\nreal: %r" log(t % (fspath, p2), 1) t = "fk(%s) salt(%s) size(%d) inode(%d) fspath(%r) at(%s)" log(t % (ret[:8], salt, fsize, inode, fspath, ctx), 5) return ret WKDAYS = "Mon Tue Wed Thu Fri Sat Sun".split() MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split() RFC2822 = "%s, %02d %s %04d %02d:%02d:%02d GMT" def formatdate(ts: Optional[float] = None) -> str: # gmtime ~= datetime.fromtimestamp(ts, UTC).timetuple() y, mo, d, h, mi, s, wd, _, _ = time.gmtime(ts) return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s) def gencookie( k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = "" ) -> str: v = v.replace("%", "%25").replace(";", "%3B") if dur: exp = formatdate(time.time() + dur) else: exp = "Fri, 15 Aug 1997 01:00:00 GMT" t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s" return t % ( k, v, r, exp, "; Secure" if tls else "", txt, "Lax" if lax else "Strict", ) def gen_content_disposition(fn: str) -> str: safe = UC_CDISP_SET bsafe = BC_CDISP_SET fn = fn.replace("/", "_").replace("\\", "_") zb = fn.encode("utf-8", "xmlcharrefreplace") if not PY2: zbl = [ chr(x).encode("utf-8") if x in bsafe else "%{:02X}".format(x).encode("ascii") for x in zb ] else: zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb] ufn = b"".join(zbl).decode("ascii") afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".") while ".." in afn: afn = afn.replace("..", ".") return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn) def humansize(sz: float, terse: bool = False) -> str: for unit in HUMANSIZE_UNITS: if sz < 1024: break sz /= 1024.0 assert unit # type: ignore # !rm if terse: return "%s%s" % (str(sz)[:4].rstrip("."), unit[:1]) else: return "%s %s" % (str(sz)[:4].rstrip("."), unit) def unhumanize(sz: str) -> int: try: return int(sz) except: pass mc = sz[-1:].lower() mi = UNHUMANIZE_UNITS.get(mc, 1) return int(float(sz[:-1]) * mi) def get_spd(nbyte: int, t0: float, t: Optional[float] = None) -> str: if t is None: t = time.time() bps = nbyte / ((t - t0) or 0.001) s1 = humansize(nbyte).replace(" ", "\033[33m").replace("iB", "") s2 = humansize(bps).replace(" ", "\033[35m").replace("iB", "") return "%s \033[0m%s/s\033[0m" % (s1, s2) def s2hms(s: float, optional_h: bool = False) -> str: s = int(s) h, s = divmod(s, 3600) m, s = divmod(s, 60) if not h and optional_h: return "%d:%02d" % (m, s) return "%d:%02d:%02d" % (h, m, s) def djoin(*paths: str) -> str: """joins without adding a trailing slash on blank args""" return os.path.join(*[x for x in paths if x]) def uncyg(path: str) -> str: if len(path) < 2 or not path.startswith("/"): return path if len(path) > 2 and path[2] != "/": return path return "%s:\\%s" % (path[1], path[3:]) def undot(path: str) -> str: ret: list[str] = [] for node in path.split("/"): if node == "." or not node: continue if node == "..": if ret: ret.pop() continue ret.append(node) return "/".join(ret) def sanitize_fn(fn: str) -> str: fn = fn.replace("\\", "/").split("/")[-1] if APTL_OS: fn = sanitize_to(fn, APTL_OS) return fn.strip() def sanitize_to(fn: str, tl: dict[int, int]) -> str: fn = fn.translate(tl) if ANYWIN: bad = ["con", "prn", "aux", "nul"] for n in range(1, 10): bad += ("com%s lpt%s" % (n, n)).split(" ") if fn.lower().split(".")[0] in bad: fn = "_" + fn return fn def sanitize_vpath(vp: str) -> str: if not APTL_OS: return vp parts = vp.replace(os.sep, "/").split("/") ret = [sanitize_to(x, APTL_OS) for x in parts] return "/".join(ret) def relchk(rp: str) -> str: if "\x00" in rp: return "[nul]" if ANYWIN: if "\n" in rp or "\r" in rp: return "x\nx" p = re.sub(r'[\\:*?"<>|]', "", rp) if p != rp: return "[{}]".format(p) return "" def absreal(fpath: str) -> str: try: return fsdec(os.path.abspath(os.path.realpath(afsenc(fpath)))) except: if not WINDOWS: raise # cpython bug introduced in 3.8, still exists in 3.9.1, # some win7sp1 and win10:20H2 boxes cannot realpath a # networked drive letter such as b"n:" or b"n:\\" return os.path.abspath(os.path.realpath(fpath)) def u8safe(txt: str) -> str: try: return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace") except: return txt.encode("utf-8", "replace").decode("utf-8", "replace") def exclude_dotfiles(filepaths: list[str]) -> list[str]: return [x for x in filepaths if not x.split("/")[-1].startswith(".")] def exclude_dotfiles_ls( vfs_ls: list[tuple[str, os.stat_result]] ) -> list[tuple[str, os.stat_result]]: return [x for x in vfs_ls if not x[0].split("/")[-1].startswith(".")] def odfusion( base: Union[ODict[str, bool], ODict["LiteralString", bool]], oth: str ) -> ODict[str, bool]: # merge an "ordered set" (just a dict really) with another list of keys words0 = [x for x in oth.split(",") if x] words1 = [x for x in oth[1:].split(",") if x] ret = base.copy() if oth.startswith("+"): for k in words1: ret[k] = True # type: ignore elif oth[:1] in ("-", "/"): for k in words1: ret.pop(k, None) # type: ignore else: ret = ODict.fromkeys(words0, True) return ret # type: ignore def ipnorm(ip: str) -> str: if ":" in ip: # assume /64 clients; drop 4 groups return IPv6Address(ip).exploded[:-20] return ip def find_prefix(ips: list[str], cidrs: list[str]) -> list[str]: ret = [] for ip in ips: hit = next((x for x in cidrs if x.startswith(ip + "/") or ip == x), None) if hit: ret.append(hit) return ret def html_sh_esc(s: str) -> str: s = re.sub(RE_HTML_SH, "_", s).replace(" ", "%20") s = s.replace("\r", "_").replace("\n", "_") return s def json_hesc(s: str) -> str: return s.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026") def html_escape(s: str, quot: bool = False, crlf: bool = False) -> str: """html.escape but also newlines""" s = s.replace("&", "&").replace("<", "<").replace(">", ">") if quot: s = s.replace('"', """).replace("'", "'") if crlf: s = s.replace("\r", " ").replace("\n", " ") return s def html_bescape(s: bytes, quot: bool = False, crlf: bool = False) -> bytes: """html.escape but bytestrings""" s = s.replace(b"&", b"&").replace(b"<", b"<").replace(b">", b">") if quot: s = s.replace(b'"', b""").replace(b"'", b"'") if crlf: s = s.replace(b"\r", b" ").replace(b"\n", b" ") return s def _quotep2(txt: str) -> str: """url quoter which deals with bytes correctly""" if not txt: return "" btxt = w8enc(txt) quot = quote(btxt, safe=b"/") return w8dec(quot.replace(b" ", b"+")) # type: ignore def _quotep3(txt: str) -> str: """url quoter which deals with bytes correctly""" if not txt: return "" btxt = w8enc(txt) quot = quote(btxt, safe=b"/").encode("utf-8") return w8dec(quot.replace(b" ", b"+")) if not PY2: _uqsb = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~/" _uqtl = { n: ("%%%02X" % (n,) if n not in _uqsb else chr(n)).encode("utf-8") for n in range(256) } _uqtl[b" "] = b"+" def _quotep3b(txt: str) -> str: """url quoter which deals with bytes correctly""" if not txt: return "" btxt = w8enc(txt) if btxt.rstrip(_uqsb): lut = _uqtl btxt = b"".join([lut[ch] for ch in btxt]) return w8dec(btxt) quotep = _quotep3b _hexd = "0123456789ABCDEFabcdef" _hex2b = {(a + b).encode(): bytes.fromhex(a + b) for a in _hexd for b in _hexd} def unquote(btxt: bytes) -> bytes: h2b = _hex2b parts = iter(btxt.split(b"%")) ret = [next(parts)] for item in parts: c = h2b.get(item[:2]) if c is None: ret.append(b"%") ret.append(item) else: ret.append(c) ret.append(item[2:]) return b"".join(ret) from urllib.parse import quote_from_bytes as quote else: from urllib import quote # type: ignore # pylint: disable=no-name-in-module from urllib import unquote # type: ignore # pylint: disable=no-name-in-module quotep = _quotep2 def unquotep(txt: str) -> str: """url unquoter which deals with bytes correctly""" btxt = w8enc(txt) unq2 = unquote(btxt) return w8dec(unq2) def vroots(vp1: str, vp2: str) -> tuple[str, str]: """ input("q/w/e/r","a/s/d/e/r") output("/q/w/","/a/s/d/") """ while vp1 and vp2: zt1 = vp1.rsplit("/", 1) if "/" in vp1 else ("", vp1) zt2 = vp2.rsplit("/", 1) if "/" in vp2 else ("", vp2) if zt1[1] != zt2[1]: break vp1 = zt1[0] vp2 = zt2[0] return ( "/%s/" % (vp1,) if vp1 else "/", "/%s/" % (vp2,) if vp2 else "/", ) def vsplit(vpath: str) -> tuple[str, str]: if "/" not in vpath: return "", vpath return vpath.rsplit("/", 1) # type: ignore # vpath-join def vjoin(rd: str, fn: str) -> str: if rd and fn: return rd + "/" + fn else: return rd or fn # url-join def ujoin(rd: str, fn: str) -> str: if rd and fn: return rd.rstrip("/") + "/" + fn.lstrip("/") else: return rd or fn def str_anchor(txt) -> tuple[int, str]: if not txt: return 0, "" txt = txt.lower() a = txt.startswith("^") b = txt.endswith("$") if not b: if not a: return 1, txt # ~ return 2, txt[1:] # ^ if not a: return 3, txt[:-1] # $ return 4, txt[1:-1] # ^$ def log_reloc( log: "NamedLogger", re: dict[str, str], pm: tuple[str, str, str, tuple["VFS", str]], ap: str, vp: str, fn: str, vn: "VFS", rem: str, ) -> None: nap, nvp, nfn, (nvn, nrem) = pm t = "reloc %s:\nold ap %r\nnew ap %r\033[36m/%r\033[0m\nold vp %r\nnew vp %r\033[36m/%r\033[0m\nold fn %r\nnew fn %r\nold vfs %r\nnew vfs %r\nold rem %r\nnew rem %r" log(t % (re, ap, nap, nfn, vp, nvp, nfn, fn, nfn, vn.vpath, nvn.vpath, rem, nrem)) def pathmod( vfs: "VFS", ap: str, vp: str, mod: dict[str, str] ) -> Optional[tuple[str, str, str, tuple["VFS", str]]]: # vfs: authsrv.vfs # ap: original abspath to a file # vp: original urlpath to a file # mod: modification (ap/vp/fn) nvp = "\n" # new vpath ap = os.path.dirname(ap) vp, fn = vsplit(vp) if mod.get("fn"): fn = mod["fn"] nvp = vp for ref, k in ((ap, "ap"), (vp, "vp")): if k not in mod: continue ms = mod[k].replace(os.sep, "/") if ms.startswith("/"): np = ms elif k == "vp": np = undot(vjoin(ref, ms)) else: np = os.path.abspath(os.path.join(ref, ms)) if k == "vp": nvp = np.lstrip("/") continue # try to map abspath to vpath np = np.replace("/", os.sep) for vn_ap, vns in vfs.all_aps: if not np.startswith(vn_ap): continue zs = np[len(vn_ap) :].replace(os.sep, "/") nvp = vjoin(vns[0].vpath, zs) break if nvp == "\n": return None vn, rem = vfs.get(nvp, "*", False, False) if not vn.realpath: raise Exception("unmapped vfs") ap = vn.canonical(rem) return ap, nvp, fn, (vn, rem) def _w8dec2(txt: bytes) -> str: """decodes filesystem-bytes to wtf8""" return surrogateescape.decodefilename(txt) # type: ignore def _w8enc2(txt: str) -> bytes: """encodes wtf8 to filesystem-bytes""" return surrogateescape.encodefilename(txt) # type: ignore def _w8dec3(txt: bytes) -> str: """decodes filesystem-bytes to wtf8""" return txt.decode(FS_ENCODING, "surrogateescape") def _w8enc3(txt: str) -> bytes: """encodes wtf8 to filesystem-bytes""" return txt.encode(FS_ENCODING, "surrogateescape") def _msdec(txt: bytes) -> str: ret = txt.decode(FS_ENCODING, "surrogateescape") return ret[4:] if ret.startswith("\\\\?\\") else ret def _msaenc(txt: str) -> bytes: return txt.replace("/", "\\").encode(FS_ENCODING, "surrogateescape") def _uncify(txt: str) -> str: txt = txt.replace("/", "\\") if ":" not in txt and not txt.startswith("\\\\"): txt = absreal(txt) return txt if txt.startswith("\\\\") else "\\\\?\\" + txt def _msenc(txt: str) -> bytes: txt = txt.replace("/", "\\") if ":" not in txt and not txt.startswith("\\\\"): txt = absreal(txt) ret = txt.encode(FS_ENCODING, "surrogateescape") return ret if ret.startswith(b"\\\\") else b"\\\\?\\" + ret w8dec = _w8dec3 if not PY2 else _w8dec2 w8enc = _w8enc3 if not PY2 else _w8enc2 def w8b64dec(txt: str) -> str: """decodes base64(filesystem-bytes) to wtf8""" return w8dec(ub64dec(txt.encode("ascii"))) def w8b64enc(txt: str) -> str: """encodes wtf8 to base64(filesystem-bytes)""" return ub64enc(w8enc(txt)).decode("ascii") if not PY2 and WINDOWS: sfsenc = w8enc afsenc = _msaenc fsenc = _msenc fsdec = _msdec uncify = _uncify elif not PY2 or not WINDOWS: fsenc = afsenc = sfsenc = w8enc fsdec = w8dec uncify = str else: # moonrunes become \x3f with bytestrings, # losing mojibake support is worth def _not_actually_mbcs_enc(txt: str) -> bytes: return txt # type: ignore def _not_actually_mbcs_dec(txt: bytes) -> str: return txt # type: ignore fsenc = afsenc = sfsenc = _not_actually_mbcs_enc fsdec = _not_actually_mbcs_dec uncify = str def s3enc(mem_cur: "sqlite3.Cursor", rd: str, fn: str) -> tuple[str, str]: ret: list[str] = [] for v in [rd, fn]: try: mem_cur.execute("select * from a where b = ?", (v,)) ret.append(v) except: ret.append("//" + w8b64enc(v)) # self.log("mojien [{}] {}".format(v, ret[-1][2:])) return ret[0], ret[1] def s3dec(rd: str, fn: str) -> tuple[str, str]: return ( w8b64dec(rd[2:]) if rd.startswith("//") else rd, w8b64dec(fn[2:]) if fn.startswith("//") else fn, ) def db_ex_chk(log: "NamedLogger", ex: Exception, db_path: str) -> bool: if str(ex) != "database is locked": return False Daemon(lsof, "dbex", (log, db_path)) return True def lsof(log: "NamedLogger", abspath: str) -> None: try: rc, so, se = runcmd([b"lsof", b"-R", fsenc(abspath)], timeout=45) zs = (so.strip() + "\n" + se.strip()).strip() log("lsof %r = %s\n%s" % (abspath, rc, zs), 3) except: 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 set_ap_perms(ap: str, vf: dict[str, Any]) -> None: zb = fsenc(ap) if "chmod_f" in vf: os.chmod(zb, vf["chmod_f"]) if "chown" in vf: os.chown(zb, vf["uid"], vf["gid"]) def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes: try: return shutil.copy2(src, dst) except: # ignore failed mtime on linux+ntfs; for example: # shutil.py:437 : copystat(src, dst, follow_symlinks=follow_symlinks) # shutil.py:376 : lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns), # [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos' _, _, tb = sys.exc_info() for _, _, fun, _ in traceback.extract_tb(tb): if fun == "copystat": if log: t = "warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\n%s" log(t % (src, dst, min_ex()), 3) return dst # close enough raise def _fs_mvrm( log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any] ) -> bool: bsrc = fsenc(src) bdst = fsenc(dst) if atomic: k = "mv_re_" act = "atomic-rename" osfun = os.replace args = [bsrc, bdst] elif dst: k = "mv_re_" act = "rename" osfun = os.rename args = [bsrc, bdst] else: k = "rm_re_" act = "delete" osfun = os.unlink args = [bsrc] maxtime = flags.get(k + "t", 0.0) chill = flags.get(k + "r", 0.0) if chill < 0.001: chill = 0.1 ino = 0 t0 = now = time.time() for attempt in range(90210): try: if ino and os.stat(bsrc).st_ino != ino: t = "src inode changed; aborting %s %r" log(t % (act, src), 1) return False if (dst and not atomic) and os.path.exists(bdst): t = "something appeared at dst; aborting rename %r ==> %r" log(t % (src, dst), 1) return False osfun(*args) # type: ignore if attempt: now = time.time() t = "%sd in %.2f sec, attempt %d: %r" log(t % (act, now - t0, attempt + 1, src)) return True except OSError as ex: now = time.time() if ex.errno == errno.ENOENT: return False if not attempt and ex.errno == errno.EXDEV: t = "using copy+delete (%s)\n %s\n %s" log(t % (ex.strerror, src, dst)) osfun = shutil.move continue if now - t0 > maxtime or attempt == 90209: raise if not attempt: if not PY2: ino = os.stat(bsrc).st_ino t = "%s failed (err.%d); retrying for %d sec: %r" log(t % (act, ex.errno, maxtime + 0.99, src)) time.sleep(chill) return False # makes pylance happy def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> None: bsrc = fsenc(src) bdst = fsenc(dst) if PY2: if os.path.exists(bdst): _fs_mvrm(log, dst, "", False, flags) # unlink _fs_mvrm(log, src, dst, False, flags) # rename elif flags.get("mv_re_t"): _fs_mvrm(log, src, dst, True, flags) else: try: os.replace(bsrc, bdst) except OSError as ex: if ex.errno != errno.EXDEV: raise t = "using copy+delete (%s);\n %s\n %s" log(t % (ex.strerror, src, dst)) try: os.unlink(bdst) except: pass shutil.move(bsrc, bdst) # type: ignore def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool: if not flags.get("rm_re_t"): os.unlink(fsenc(abspath)) return True return _fs_mvrm(log, abspath, "", False, flags) 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: # strip leafs until it hits an existing folder ap = ap.rsplit(BOS_SEP, 1)[0] if ANYWIN: assert ctypes # type: ignore # !rm abspath = fsdec(ap) bfree = ctypes.c_ulonglong(0) btotal = ctypes.c_ulonglong(0) bavail = ctypes.c_ulonglong(0) wk32.GetDiskFreeSpaceExW( # type: ignore ctypes.c_wchar_p(abspath), ctypes.pointer(bavail), ctypes.pointer(btotal), ctypes.pointer(bfree), ) return (bavail.value, btotal.value, "") else: sv = os.statvfs(ap) free = sv.f_frsize * sv.f_bavail total = sv.f_frsize * sv.f_blocks return (free, total, "") except Exception as ex: return (0, 0, repr(ex)) if not ANYWIN and not MACOS: def siocoutq(sck: socket.socket) -> int: assert fcntl # type: ignore # !rm assert termios # type: ignore # !rm # SIOCOUTQ^sockios.h == TIOCOUTQ^ioctl.h try: zb = fcntl.ioctl(sck.fileno(), termios.TIOCOUTQ, b"AAAA") return sunpack(b"I", zb)[0] # type: ignore except: return 1 else: # macos: getsockopt(fd, SOL_SOCKET, SO_NWRITE, ...) # windows: TcpConnectionEstatsSendBuff def siocoutq(sck: socket.socket) -> int: return 1 def shut_socket(log: "NamedLogger", sck: socket.socket, timeout: int = 3) -> None: t0 = time.time() fd = sck.fileno() if fd == -1: sck.close() return try: sck.settimeout(timeout) sck.shutdown(socket.SHUT_WR) try: while time.time() - t0 < timeout: if not siocoutq(sck): # kernel says tx queue empty, we good break # on windows in particular, drain rx until client shuts if not sck.recv(32 * 1024): break sck.shutdown(socket.SHUT_RDWR) except: pass except OSError as ex: log("shut(%d): ok; client has already disconnected; %s" % (fd, ex.errno), "90") except Exception as ex: log("shut({}): {}".format(fd, ex), "90") finally: td = time.time() - t0 if td >= 1: log("shut({}) in {:.3f} sec".format(fd, td), "90") sck.close() def read_socket( sr: Unrecv, bufsz: int, total_size: int ) -> Generator[bytes, None, None]: remains = total_size while remains > 0: if bufsz > remains: bufsz = remains try: buf = sr.recv(bufsz) except OSError: t = "client d/c during binary post after {} bytes, {} bytes remaining" raise Pebkac(400, t.format(total_size - remains, remains)) remains -= len(buf) yield buf def read_socket_unbounded(sr: Unrecv, bufsz: int) -> Generator[bytes, None, None]: try: while True: yield sr.recv(bufsz) except: return def read_socket_chunked( sr: Unrecv, bufsz: int, log: Optional["NamedLogger"] = None ) -> Generator[bytes, None, None]: err = "upload aborted: expected chunk length, got [{}] |{}| instead" while True: buf = b"" while b"\r" not in buf: try: buf += sr.recv(2) if len(buf) > 16: raise Exception() except: err = err.format(buf.decode("utf-8", "replace"), len(buf)) raise Pebkac(400, err) if not buf.endswith(b"\n"): sr.recv(1) try: chunklen = int(buf.rstrip(b"\r\n"), 16) except: err = err.format(buf.decode("utf-8", "replace"), len(buf)) raise Pebkac(400, err) if chunklen == 0: x = sr.recv_ex(2, False) if x == b"\r\n": sr.te = 2 return t = "protocol error after final chunk: want b'\\r\\n', got {!r}" raise Pebkac(400, t.format(x)) if log: log("receiving %d byte chunk" % (chunklen,)) for chunk in read_socket(sr, bufsz, chunklen): yield chunk x = sr.recv_ex(2, False) if x != b"\r\n": t = "protocol error in chunk separator: want b'\\r\\n', got {!r}" raise Pebkac(400, t.format(x)) def list_ips() -> list[str]: ret: set[str] = set() for nic in get_adapters(): for ipo in nic.ips: if len(ipo.ip) < 7: ret.add(ipo.ip[0]) # ipv6 is (ip,0,0) else: ret.add(ipo.ip) return list(ret) def build_netmap(csv: str, defer_mutex: bool = False): csv = csv.lower().strip() if csv in ("any", "all", "no", ",", ""): return None srcs = [x.strip() for x in csv.split(",") if x.strip()] expanded_shorthands = False for shorthand in ("lan", "local", "private", "prvt"): if shorthand in srcs: if not expanded_shorthands: srcs += [ # lan: "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8", # link-local: "169.254.0.0/16", "fe80::/10", # loopback: "127.0.0.0/8", "::1/128", ] expanded_shorthands = True srcs.remove(shorthand) if not HAVE_IPV6: srcs = [x for x in srcs if ":" not in x] cidrs = [] for zs in srcs: if not zs.endswith("."): cidrs.append(zs) continue # translate old syntax "172.19." => "172.19.0.0/16" words = len(zs.rstrip(".").split(".")) if words == 1: zs += "0.0.0/8" elif words == 2: zs += "0.0/16" elif words == 3: zs += "0/24" else: raise Exception("invalid config value [%s]" % (zs,)) cidrs.append(zs) ips = [x.split("/")[0] for x in cidrs] return NetMap(ips, cidrs, True, False, defer_mutex) def load_ipu( log: "RootLogger", ipus: list[str], defer_mutex: bool = False ) -> tuple[dict[str, str], NetMap]: ip_u = {"": "*"} cidr_u = {} for ipu in ipus: try: cidr, uname = ipu.split("=") cip, csz = cidr.split("/") except: t = "\n invalid value %r for argument --ipu; must be CIDR=UNAME (192.168.0.0/16=amelia)" raise Exception(t % (ipu,)) uname2 = cidr_u.get(cidr) if uname2 is not None: t = "\n invalid value %r for argument --ipu; cidr %s already mapped to %r" raise Exception(t % (ipu, cidr, uname2)) cidr_u[cidr] = uname ip_u[cip] = uname try: nm = NetMap(["::"], list(cidr_u.keys()), True, True, defer_mutex) except Exception as ex: t = "failed to translate --ipu into netmap, probably due to invalid config: %r" log("root", t % (ex,), 1) raise return ip_u, nm def load_ipr( log: "RootLogger", iprs: list[str], defer_mutex: bool = False ) -> dict[str, NetMap]: ret = {} for ipr in iprs: try: zs, uname = ipr.split("=") cidrs = zs.split(",") except: t = "\n invalid value %r for argument --ipr; must be CIDR[,CIDR[,...]]=UNAME (192.168.0.0/16=amelia)" raise Exception(t % (ipr,)) try: nm = NetMap(["::"], cidrs, True, True, defer_mutex) except Exception as ex: t = "failed to translate --ipr into netmap, probably due to invalid config: %r" log("root", t % (ex,), 1) raise ret[uname] = nm return ret def yieldfile(fn: str, bufsz: int) -> Generator[bytes, None, None]: readsz = min(bufsz, 128 * 1024) with open(fsenc(fn), "rb", bufsz) as f: while True: buf = f.read(readsz) if not buf: break yield buf def justcopy( fin: Generator[bytes, None, None], fout: Union[typing.BinaryIO, typing.IO[Any]], hashobj: Optional["hashlib._Hash"], max_sz: int, slp: float, ) -> tuple[int, str, str]: tlen = 0 for buf in fin: tlen += len(buf) if max_sz and tlen > max_sz: continue fout.write(buf) if slp: time.sleep(slp) return tlen, "checksum-disabled", "checksum-disabled" def eol_conv( fin: Generator[bytes, None, None], conv: str ) -> Generator[bytes, None, None]: crlf = conv.lower() == "crlf" for buf in fin: buf = buf.replace(b"\r", b"") if crlf: buf = buf.replace(b"\n", b"\r\n") yield buf def hashcopy( fin: Generator[bytes, None, None], fout: Union[typing.BinaryIO, typing.IO[Any]], hashobj: Optional["hashlib._Hash"], max_sz: int, slp: float, ) -> tuple[int, str, str]: if not hashobj: hashobj = hashlib.sha512() tlen = 0 for buf in fin: tlen += len(buf) if max_sz and tlen > max_sz: continue hashobj.update(buf) fout.write(buf) if slp: time.sleep(slp) digest_b64 = ub64enc(hashobj.digest()[:33]).decode("ascii") return tlen, hashobj.hexdigest(), digest_b64 def sendfile_py( log: "NamedLogger", lower: int, upper: int, f: typing.BinaryIO, s: socket.socket, bufsz: int, slp: float, use_poll: bool, dls: dict[str, tuple[float, int]], dl_id: str, ) -> int: sent = 0 remains = upper - lower f.seek(lower) while remains > 0: if slp: time.sleep(slp) buf = f.read(min(bufsz, remains)) if not buf: return remains try: s.sendall(buf) remains -= len(buf) except: return remains if dl_id: sent += len(buf) dls[dl_id] = (time.time(), sent) return 0 def sendfile_kern( log: "NamedLogger", lower: int, upper: int, f: typing.BinaryIO, s: socket.socket, bufsz: int, slp: float, use_poll: bool, dls: dict[str, tuple[float, int]], dl_id: str, ) -> int: out_fd = s.fileno() in_fd = f.fileno() ofs = lower stuck = 0.0 if use_poll: poll = select.poll() poll.register(out_fd, select.POLLOUT) while ofs < upper: stuck = stuck or time.time() try: req = min(0x2000000, upper - ofs) # 32 MiB if use_poll: poll.poll(10000) # type: ignore else: select.select([], [out_fd], [], 10) n = os.sendfile(out_fd, in_fd, ofs, req) stuck = 0 except OSError as ex: # client stopped reading; do another select d = time.time() - stuck if d < 3600 and ex.errno == errno.EWOULDBLOCK: time.sleep(0.02) continue n = 0 except Exception as ex: n = 0 d = time.time() - stuck log("sendfile failed after {:.3f} sec: {!r}".format(d, ex)) if n <= 0: return upper - ofs ofs += n if dl_id: dls[dl_id] = (time.time(), ofs - lower) # print("sendfile: ok, sent {} now, {} total, {} remains".format(n, ofs - lower, upper - ofs)) return 0 def statdir( logger: Optional["RootLogger"], scandir: bool, lstat: bool, top: str, throw: bool ) -> Generator[tuple[str, os.stat_result], None, None]: if lstat and ANYWIN: lstat = False if lstat and (PY2 or os.stat not in os.supports_follow_symlinks): scandir = False src = "statdir" try: btop = fsenc(top) if scandir and hasattr(os, "scandir"): src = "scandir" with os.scandir(btop) as dh: for fh in dh: try: yield (fsdec(fh.name), fh.stat(follow_symlinks=not lstat)) except Exception as ex: if not logger: continue logger(src, "[s] {} @ {}".format(repr(ex), fsdec(fh.path)), 6) else: src = "listdir" fun: Any = os.lstat if lstat else os.stat btop_ = os.path.join(btop, b"") for name in os.listdir(btop): abspath = btop_ + name try: yield (fsdec(name), fun(abspath)) except Exception as ex: if not logger: continue logger(src, "[s] {} @ {}".format(repr(ex), fsdec(abspath)), 6) except Exception as ex: if throw: zi = getattr(ex, "errno", 0) if zi == errno.ENOENT: raise Pebkac(404, str(ex)) raise t = "{} @ {}".format(repr(ex), top) if logger: logger(src, t, 1) else: print(t) def dir_is_empty(logger: "RootLogger", scandir: bool, top: str): for _ in statdir(logger, scandir, False, top, False): return False return True def rmdirs( logger: "RootLogger", scandir: bool, lstat: bool, top: str, depth: int ) -> tuple[list[str], list[str]]: """rmdir all descendants, then self""" if not os.path.isdir(fsenc(top)): top = os.path.dirname(top) depth -= 1 stats = statdir(logger, scandir, lstat, top, False) dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)] if dirs: top_ = os.path.join(top, "") dirs = [top_ + x for x in dirs] ok = [] ng = [] for d in reversed(dirs): a, b = rmdirs(logger, scandir, lstat, d, depth + 1) ok += a ng += b if depth: try: os.rmdir(fsenc(top)) ok.append(top) except: ng.append(top) return ok, ng def rmdirs_up(top: str, stop: str) -> tuple[list[str], list[str]]: """rmdir on self, then all parents""" if top == stop: return [], [top] try: os.rmdir(fsenc(top)) except: return [], [top] par = os.path.dirname(top) if not par or par == stop: return [top], [] ok, ng = rmdirs_up(par, stop) return [top] + ok, ng def unescape_cookie(orig: str, name: str) -> str: # mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn if not name.startswith("cppw"): orig = orig[:3] ret = [] esc = "" for ch in orig: if ch == "%": if esc: ret.append(esc) esc = ch elif esc: esc += ch if len(esc) == 3: try: ret.append(chr(int(esc[1:], 16))) except: ret.append(esc) esc = "" else: ret.append(ch) if esc: ret.append(esc) return "".join(ret) def guess_mime( url: str, path: str = "", fallback: str = "application/octet-stream" ) -> str: try: ext = url.rsplit(".", 1)[1].lower() except: ext = "" ret = MIMES.get(ext) if not ret: x = mimetypes.guess_type(url) ret = "application/{}".format(x[1]) if x[1] else x[0] if not ret and path: try: with open(fsenc(path), "rb", 0) as f: ret = magic.from_buffer(f.read(4096), mime=True) if ret.startswith("text/htm"): # avoid serving up HTML content unless there was actually a .html extension ret = "text/plain" except Exception as ex: pass if not ret: ret = fallback if ";" not in ret: if ret.startswith("text/") or ret.endswith("/javascript"): ret += "; charset=utf-8" return ret def safe_mime(mime: str) -> str: if "text/" in mime or "xml" in mime: return "text/plain; charset=utf-8" else: return "application/octet-stream" def getalive(pids: list[int], pgid: int) -> list[int]: alive = [] for pid in pids: try: if pgid: # check if still one of ours if os.getpgid(pid) == pgid: alive.append(pid) else: # windows doesn't have pgroups; assume assert psutil # type: ignore # !rm psutil.Process(pid) alive.append(pid) except: pass return alive def killtree(root: int) -> None: """still racy but i tried""" try: # limit the damage where possible (unixes) pgid = os.getpgid(os.getpid()) except: pgid = 0 if HAVE_PSUTIL: assert psutil # type: ignore # !rm pids = [root] parent = psutil.Process(root) for child in parent.children(recursive=True): pids.append(child.pid) child.terminate() parent.terminate() parent = None elif pgid: # linux-only pids = [] chk = [root] while chk: pid = chk[0] chk = chk[1:] pids.append(pid) _, t, _ = runcmd(["pgrep", "-P", str(pid)]) chk += [int(x) for x in t.strip().split("\n") if x] pids = getalive(pids, pgid) # filter to our pgroup for pid in pids: os.kill(pid, signal.SIGTERM) else: # windows gets minimal effort sorry os.kill(root, signal.SIGTERM) return for n in range(10): time.sleep(0.1) pids = getalive(pids, pgid) if not pids or n > 3 and pids == [root]: break for pid in pids: try: os.kill(pid, signal.SIGKILL) except: pass def _find_nice() -> str: if WINDOWS: return "" # use creationflags try: zs = shutil.which("nice") if zs: return zs except: pass # busted PATHs and/or py2 for zs in ("/bin", "/sbin", "/usr/bin", "/usr/sbin"): zs += "/nice" if os.path.exists(zs): return zs return "" NICES = _find_nice() NICEB = NICES.encode("utf-8") def runcmd( argv: Union[list[bytes], list[str], list["LiteralString"]], timeout: Optional[float] = None, **ka: Any ) -> tuple[int, str, str]: isbytes = isinstance(argv[0], (bytes, bytearray)) oom = ka.pop("oom", 0) # 0..1000 kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both sin: Optional[bytes] = ka.pop("sin", None) if sin: ka["stdin"] = sp.PIPE cout = sp.PIPE if capture in [1, 3] else None cerr = sp.PIPE if capture in [2, 3] else None bout: bytes berr: bytes if ANYWIN: if isbytes: if argv[0] in CMD_EXEB: argv[0] += b".exe" # type: ignore else: if argv[0] in CMD_EXES: argv[0] += ".exe" # type: ignore if ka.pop("nice", None): if WINDOWS: ka["creationflags"] = 0x4000 elif NICEB: if isbytes: argv = [NICEB] + argv # type: ignore else: argv = [NICES] + argv # type: ignore p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka) if oom and not ANYWIN and not MACOS: try: with open("/proc/%d/oom_score_adj" % (p.pid,), "wb") as f: f.write(("%d\n" % (oom,)).encode("utf-8")) except: pass if not timeout or PY2: bout, berr = p.communicate(sin) # type: ignore else: try: bout, berr = p.communicate(sin, timeout=timeout) # type: ignore except sp.TimeoutExpired: if kill == "n": return -18, "", "" # SIGCONT; leave it be elif kill == "m": p.kill() else: killtree(p.pid) try: bout, berr = p.communicate(timeout=1) # type: ignore except: bout = b"" berr = b"" stdout = bout.decode("utf-8", "replace") if cout else "" stderr = berr.decode("utf-8", "replace") if cerr else "" rc: int = p.returncode if rc is None: rc = -14 # SIGALRM; failed to kill return rc, stdout, stderr def chkcmd(argv: Union[list[bytes], list[str]], **ka: Any) -> tuple[str, str]: ok, sout, serr = runcmd(argv, **ka) if ok != 0: retchk(ok, argv, serr) raise Exception(serr) return sout, serr def mchkcmd(argv: Union[list[bytes], list[str]], timeout: float = 10) -> None: if PY2: with open(os.devnull, "wb") as f: rv = sp.call(argv, stdout=f, stderr=f) else: rv = sp.call(argv, stdout=sp.DEVNULL, stderr=sp.DEVNULL, timeout=timeout) if rv: raise sp.CalledProcessError(rv, (argv[0], b"...", argv[-1])) def retchk( rc: int, cmd: Union[list[bytes], list[str]], serr: str, logger: Optional["NamedLogger"] = None, color: Union[int, str] = 0, verbose: bool = False, ) -> None: if rc < 0: rc = 128 - rc if not rc or rc < 126 and not verbose: return s = None if rc > 128: try: s = str(signal.Signals(rc - 128)) except: pass elif rc == 126: s = "invalid program" elif rc == 127: s = "program not found" elif verbose: s = "unknown" else: s = "invalid retcode" if s: t = "{} <{}>".format(rc, s) else: t = str(rc) try: c = " ".join([fsdec(x) for x in cmd]) # type: ignore except: c = str(cmd) t = "error {} from [{}]".format(t, c) if serr: if len(serr) > 8192: zs = "%s\n[ ...TRUNCATED... ]\n%s\n[ NOTE: full msg was %d chars ]" serr = zs % (serr[:4096], serr[-4096:].rstrip(), len(serr)) serr = serr.replace("\n", "\nstderr: ") t += "\nstderr: " + serr if logger: logger(t, color) else: raise Exception(t) def _parsehook( log: Optional["NamedLogger"], cmd: str ) -> tuple[str, bool, bool, bool, bool, bool, float, dict[str, Any], list[str]]: areq = "" chk = False fork = False jtxt = False imp = False sin = False wait = 0.0 tout = 0.0 kill = "t" cap = 0 ocmd = cmd while "," in cmd[:6]: arg, cmd = cmd.split(",", 1) if arg == "c": chk = True elif arg == "f": fork = True elif arg == "j": jtxt = True elif arg == "I": imp = True elif arg == "s": sin = True elif arg.startswith("w"): wait = float(arg[1:]) elif arg.startswith("t"): tout = float(arg[1:]) elif arg.startswith("c"): cap = int(arg[1:]) # 0=none 1=stdout 2=stderr 3=both elif arg.startswith("k"): kill = arg[1:] # [t]ree [m]ain [n]one elif arg.startswith("a"): areq = arg[1:] # required perms elif arg.startswith("i"): pass elif not arg: break else: t = "hook: invalid flag {} in {}" (log or print)(t.format(arg, ocmd)) env = os.environ.copy() try: if EXE: raise Exception() pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) zsl = [str(pypath)] + [str(x) for x in sys.path if x] pypath = str(os.pathsep.join(zsl)) env["PYTHONPATH"] = pypath except: if not EXE: raise sp_ka = { "env": env, "nice": True, "oom": 300, "timeout": tout, "kill": kill, "capture": cap, } argv = cmd.split(",") if "," in cmd else [cmd] argv[0] = os.path.expandvars(os.path.expanduser(argv[0])) return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv def runihook( log: Optional["NamedLogger"], verbose: bool, cmd: str, vol: "VFS", ups: list[tuple[str, int, int, str, str, str, int, str]], ) -> bool: _, chk, _, fork, _, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) bcmd = [sfsenc(x) for x in acmd] if acmd[0].endswith(".py"): bcmd = [sfsenc(pybin)] + bcmd vps = [vjoin(*list(s3dec(x[3], x[4]))) for x in ups] aps = [djoin(vol.realpath, x) for x in vps] if jtxt: # 0w 1mt 2sz 3rd 4fn 5ip 6at ja = [ { "ap": uncify(ap), # utf8 for json "vp": vp, "wark": x[0][:16], "mt": x[1], "sz": x[2], "ip": x[5], "at": x[6], } for x, vp, ap in zip(ups, vps, aps) ] sp_ka["sin"] = json.dumps(ja).encode("utf-8", "replace") else: sp_ka["sin"] = b"\n".join(fsenc(x) for x in aps) if acmd[0].startswith("zmq:"): try: msg = sp_ka["sin"].decode("utf-8", "replace") _zmq_hook(log, verbose, "xiu", acmd[0][4:].lower(), msg, wait, sp_ka) if verbose and log: log("hook(xiu) %r OK" % (cmd,), 6) except Exception as ex: if log: log("zeromq failed: %r" % (ex,)) return True t0 = time.time() if fork: Daemon(runcmd, cmd, bcmd, ka=sp_ka) else: rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore if chk and rc: retchk(rc, bcmd, err, log, 5) return False if wait: wait -= time.time() - t0 if wait > 0: time.sleep(wait) return True ZMQ = {} ZMQ_DESC = { "pub": "fire-and-forget to all/any connected SUB-clients", "push": "fire-and-forget to one of the connected PULL-clients", "req": "send messages to a REP-server and blocking-wait for ack", } def _zmq_hook( log: Optional["NamedLogger"], verbose: bool, src: str, cmd: str, msg: str, wait: float, sp_ka: dict[str, Any], ) -> tuple[int, str]: import zmq try: mtx = ZMQ["mtx"] except: ZMQ["mtx"] = threading.Lock() time.sleep(0.1) mtx = ZMQ["mtx"] ret = "" nret = 0 t0 = time.time() if verbose and log: log("hook(%s) %r entering zmq-main-lock" % (src, cmd), 6) with mtx: try: mode, sck, mtx = ZMQ[cmd] except: mode, uri = cmd.split(":", 1) try: desc = ZMQ_DESC[mode] if log: t = "libzmq(%s) pyzmq(%s) init(%s); %s" log(t % (zmq.zmq_version(), zmq.__version__, cmd, desc)) except: raise Exception("the only supported ZMQ modes are REQ PUB PUSH") try: ctx = ZMQ["ctx"] except: ctx = ZMQ["ctx"] = zmq.Context() timeout = sp_ka["timeout"] if mode == "pub": sck = ctx.socket(zmq.PUB) sck.setsockopt(zmq.LINGER, 0) sck.bind(uri) time.sleep(1) # give clients time to connect; avoids losing first msg elif mode == "push": sck = ctx.socket(zmq.PUSH) if timeout: sck.SNDTIMEO = int(timeout * 1000) sck.setsockopt(zmq.LINGER, 0) sck.bind(uri) elif mode == "req": sck = ctx.socket(zmq.REQ) if timeout: sck.RCVTIMEO = int(timeout * 1000) sck.setsockopt(zmq.LINGER, 0) sck.connect(uri) else: raise Exception() mtx = threading.Lock() ZMQ[cmd] = (mode, sck, mtx) if verbose and log: log("hook(%s) %r entering socket-lock" % (src, cmd), 6) with mtx: if verbose and log: log("hook(%s) %r sending |%d|" % (src, cmd, len(msg)), 6) sck.send_string(msg) # PUSH can safely timeout here if mode == "req": if verbose and log: log("hook(%s) %r awaiting ack from req" % (src, cmd), 6) try: ret = sck.recv().decode("utf-8", "replace") if ret.startswith("return "): m = re.search("^return ([0-9]+)", ret[:12]) if m: nret = int(m.group(1)) except: sck.close() del ZMQ[cmd] # bad state; must reset raise Exception("ack timeout; zmq socket killed") if ret and log: log("hook(%s) %r ACK: %r" % (src, cmd, ret), 6) if wait: wait -= time.time() - t0 if wait > 0: time.sleep(wait) return nret, ret def _runhook( log: Optional["NamedLogger"], verbose: bool, src: str, cmd: str, ap: str, vp: str, host: str, uname: str, perms: str, mt: float, sz: int, ip: str, at: float, txt: Optional[list[str]], ) -> dict[str, Any]: ret = {"rc": 0} areq, chk, imp, fork, sin, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd) if areq: for ch in areq: if ch not in perms: t = "user %s not allowed to run hook %s; need perms %s, have %s" if log: log(t % (uname, cmd, areq, perms)) return ret # fallthrough to next hook if imp or jtxt: ja = { "ap": ap, "vp": vp, "mt": mt, "sz": sz, "ip": ip, "at": at or time.time(), "host": host, "user": uname, "perms": perms, "src": src, } if txt: ja["txt"] = txt[0] ja["body"] = txt[1] if imp: ja["log"] = log mod = loadpy(acmd[0], False) return mod.main(ja) arg = json.dumps(ja) else: arg = txt[0] if txt else ap if acmd[0].startswith("zmq:"): zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka) if zi: raise Exception("zmq says %d" % (zi,)) try: ret = json.loads(zs) if "rc" not in ret: ret["rc"] = 0 return ret except: return {"rc": 0, "stdout": zs} if sin: sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace") else: acmd += [arg] if acmd[0].endswith(".py"): acmd = [pybin] + acmd bcmd = [fsenc(x) if x == ap else sfsenc(x) for x in acmd] t0 = time.time() if fork: Daemon(runcmd, cmd, [bcmd], ka=sp_ka) else: rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore if chk and rc: ret["rc"] = rc zi = 0 if rc == 100 else rc retchk(zi, bcmd, err, log, 5) else: try: ret = json.loads(v) except: pass try: if "stdout" not in ret: ret["stdout"] = v if "stderr" not in ret: ret["stderr"] = err if "rc" not in ret: ret["rc"] = rc except: ret = {"rc": rc, "stdout": v, "stderr": err} if wait: wait -= time.time() - t0 if wait > 0: time.sleep(wait) return ret def runhook( log: Optional["NamedLogger"], broker: Optional["BrokerCli"], up2k: Optional["Up2k"], src: str, cmds: list[str], ap: str, vp: str, host: str, uname: str, perms: str, mt: float, sz: int, ip: str, at: float, txt: Optional[list[str]], ) -> dict[str, Any]: assert broker or up2k # !rm args = (broker or up2k).args # type: ignore verbose = args.hook_v vp = vp.replace("\\", "/") ret = {"rc": 0} stop = False for cmd in cmds: try: hr = _runhook( log, verbose, src, cmd, ap, vp, host, uname, perms, mt, sz, ip, at, txt ) if verbose and log: log("hook(%s) %r => \033[32m%s" % (src, cmd, hr), 6) for k, v in hr.items(): if k in ("idx", "del") and v: if broker: broker.say("up2k.hook_fx", k, v, vp) else: assert up2k # !rm up2k.fx_backlog.append((k, v, vp)) elif k == "reloc" and v: # idk, just take the last one ig ret["reloc"] = v elif k == "rc" and v: stop = True ret[k] = 0 if v == 100 else v elif k in ret: if k == "stdout" and v and not ret[k]: ret[k] = v else: ret[k] = v except Exception as ex: (log or print)("hook: %r, %s" % (ex, ex)) if ",c," in "," + cmd: return {"rc": 1} break if stop: break return ret def loadpy(ap: str, hot: bool) -> Any: """ a nice can of worms capable of causing all sorts of bugs depending on what other inconveniently named files happen to be in the same folder """ ap = os.path.expandvars(os.path.expanduser(ap)) mdir, mfile = os.path.split(absreal(ap)) mname = mfile.rsplit(".", 1)[0] sys.path.insert(0, mdir) if PY2: mod = __import__(mname) if hot: reload(mod) # type: ignore else: import importlib mod = importlib.import_module(mname) if hot: importlib.reload(mod) sys.path.remove(mdir) return mod def gzip_orig_sz(fn: str) -> int: with open(fsenc(fn), "rb") as f: return gzip_file_orig_sz(f) def gzip_file_orig_sz(f) -> int: start = f.tell() f.seek(-4, 2) rv = f.read(4) f.seek(start, 0) return sunpack(b"I", rv)[0] # type: ignore def align_tab(lines: list[str]) -> list[str]: rows = [] ncols = 0 for ln in lines: row = [x for x in ln.split(" ") if x] ncols = max(ncols, len(row)) rows.append(row) lens = [0] * ncols for row in rows: for n, col in enumerate(row): lens[n] = max(lens[n], len(col)) return ["".join(x.ljust(y + 2) for x, y in zip(row, lens)) for row in rows] def visual_length(txt: str) -> int: # from r0c eoc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" clen = 0 pend = None counting = True for ch in txt: # escape sequences can never contain ESC; # treat pend as regular text if so if ch == "\033" and pend: clen += len(pend) counting = True pend = None if not counting: if ch in eoc: counting = True else: if pend: pend += ch if pend.startswith("\033["): counting = False else: clen += len(pend) counting = True pend = None else: if ch == "\033": pend = "%s" % (ch,) else: co = ord(ch) # the safe parts of latin1 and cp437 (no greek stuff) if ( co < 0x100 # ascii + lower half of latin1 or (co >= 0x2500 and co <= 0x25A0) # box drawings or (co >= 0x2800 and co <= 0x28FF) # braille ): clen += 1 else: # assume moonrunes or other double-width clen += 2 return clen def wrap(txt: str, maxlen: int, maxlen2: int) -> list[str]: # from r0c words = re.sub(r"([, ])", r"\1\n", txt.rstrip()).split("\n") pad = maxlen - maxlen2 ret = [] for word in words: if len(word) * 2 < maxlen or visual_length(word) < maxlen: ret.append(word) else: while visual_length(word) >= maxlen: ret.append(word[: maxlen - 1] + "-") word = word[maxlen - 1 :] if word: ret.append(word) words = ret ret = [] ln = "" spent = 0 for word in words: wl = visual_length(word) if spent + wl > maxlen: ret.append(ln) maxlen = maxlen2 spent = 0 ln = " " * pad ln += word spent += wl if ln: ret.append(ln) return ret def termsize() -> tuple[int, int]: try: w, h = os.get_terminal_size() return w, h except: pass env = os.environ def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]: assert fcntl # type: ignore # !rm assert termios # type: ignore # !rm try: cr = sunpack(b"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"AAAA")) return cr[::-1] except: return None cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = ioctl_GWINSZ(fd) os.close(fd) except: pass try: return cr or (int(env["COLUMNS"]), int(env["LINES"])) except: return 80, 25 def hidedir(dp) -> None: if ANYWIN: try: assert wk32 # type: ignore # !rm k32 = wk32 attrs = k32.GetFileAttributesW(dp) if attrs >= 0: k32.SetFileAttributesW(dp, attrs | 2) except: pass _flocks = {} def _lock_file_noop(ap: str) -> bool: return True def _lock_file_ioctl(ap: str) -> bool: assert fcntl # type: ignore # !rm try: fd = _flocks.pop(ap) os.close(fd) except: pass fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438) # NOTE: the fcntl.lockf identifier is (pid,node); # the lock will be dropped if os.close(os.open(ap)) # is performed anywhere else in this thread try: fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) _flocks[ap] = fd return True except Exception as ex: eno = getattr(ex, "errno", -1) try: os.close(fd) except: pass if eno in (errno.EAGAIN, errno.EACCES): return False print("WARNING: unexpected errno %d from fcntl.lockf; %r" % (eno, ex)) return True def _lock_file_windows(ap: str) -> bool: try: import msvcrt try: fd = _flocks.pop(ap) os.close(fd) except: pass fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438) msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) return True except Exception as ex: eno = getattr(ex, "errno", -1) if eno == errno.EACCES: return False print("WARNING: unexpected errno %d from msvcrt.locking; %r" % (eno, ex)) return True if os.environ.get("PRTY_NO_DB_LOCK"): lock_file = _lock_file_noop elif ANYWIN: lock_file = _lock_file_windows elif HAVE_FCNTL: lock_file = _lock_file_ioctl else: lock_file = _lock_file_noop def _open_nolock_windows(bap: Union[str, bytes], *a, **ka) -> typing.BinaryIO: assert ctypes # !rm assert wk32 # !rm import msvcrt try: ap = bap.decode("utf-8", "replace") # type: ignore except: ap = bap fh = wk32.CreateFileW(ap, 0x80000000, 7, None, 3, 0x80, None) # `-ap, GENERIC_READ, FILE_SHARE_READ|WRITE|DELETE, None, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, None if fh == -1: ec = ctypes.get_last_error() # type: ignore raise ctypes.WinError(ec) # type: ignore fd = msvcrt.open_osfhandle(fh, os.O_RDONLY) # type: ignore return os.fdopen(fd, "rb") if ANYWIN: open_nolock = _open_nolock_windows else: open_nolock = open try: if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"): # py3.8 doesn't have .files # py3.9 has broken .is_file raise ImportError() import importlib.resources as impresources except ImportError: try: import importlib_resources as impresources except ImportError: impresources = None try: if sys.version_info > (3, 10): raise ImportError() import pkg_resources except ImportError: pkg_resources = None def _pkg_resource_exists(pkg: str, name: str) -> bool: if not pkg_resources: return False try: return pkg_resources.resource_exists(pkg, name) except NotImplementedError: return False def stat_resource(E: EnvParams, name: str): path = E.mod_ + name if os.path.exists(path): return os.stat(fsenc(path)) return None def _find_impresource(pkg: types.ModuleType, name: str): assert impresources # !rm try: files = impresources.files(pkg) except ImportError: return None return files.joinpath(name) _rescache_has = {} def _has_resource(name: str): try: return _rescache_has[name] except: pass if len(_rescache_has) > 999: _rescache_has.clear() assert __package__ # !rm pkg = sys.modules[__package__] if impresources: res = _find_impresource(pkg, name) if res and res.is_file(): _rescache_has[name] = True return True if pkg_resources: if _pkg_resource_exists(pkg.__name__, name): _rescache_has[name] = True return True _rescache_has[name] = False return False def has_resource(E: EnvParams, name: str): return _has_resource(name) or os.path.exists(E.mod_ + name) def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]: enc = None if "b" in mode else "utf-8" if impresources: assert __package__ # !rm res = _find_impresource(sys.modules[__package__], name) if res and res.is_file(): if enc: return res.open(mode, encoding=enc) else: # throws if encoding= is mentioned at all return res.open(mode) if pkg_resources: assert __package__ # !rm pkg = sys.modules[__package__] if _pkg_resource_exists(pkg.__name__, name): stream = pkg_resources.resource_stream(pkg.__name__, name) if enc: stream = codecs.getreader(enc)(stream) return stream ap = E.mod_ + name if PY2: return codecs.open(ap, "r", encoding=enc) # type: ignore return open(ap, mode, encoding=enc) class Pebkac(Exception): def __init__( self, code: int, msg: Optional[str] = None, log: Optional[str] = None ) -> None: super(Pebkac, self).__init__(msg or HTTPCODE[code]) self.code = code self.log = log def __repr__(self) -> str: return "Pebkac({}, {})".format(self.code, repr(self.args)) class WrongPostKey(Pebkac): def __init__( self, expected: str, got: str, fname: Optional[str], datagen: Generator[bytes, None, None], ) -> None: msg = 'expected field "{}", got "{}"'.format(expected, got) super(WrongPostKey, self).__init__(422, msg) self.expected = expected self.got = got self.fname = fname self.datagen = datagen _: Any = ( gzip, mp, zlib, BytesIO, quote, unquote, SQLITE_VER, JINJA_VER, PYFTPD_VER, PARTFTPY_VER, ) __all__ = [ "gzip", "mp", "zlib", "BytesIO", "quote", "unquote", "SQLITE_VER", "JINJA_VER", "PYFTPD_VER", "PARTFTPY_VER", ] ================================================ FILE: copyparty/web/Makefile ================================================ # run me to zopfli all the static files # which should help on really slow connections # but then why are you using copyparty in the first place pk: $(addsuffix .gz, $(wildcard tl/*.js *.js *.css) \ a/webdav-cfg.txt ) un: $(addsuffix .un, $(wildcard tl/*.gz *.gz a/*.gz)) tl/%.js.gz: tl/%.js ./Makefile.s1 <$< | pigz -c11 -J 34 -I 573 >$@ touch -r $< $@ rm $< %.gz: % pigz -c11 -J 34 -I 573 <$< >$@ touch -r $< $@ rm $< %.un: % pigz -d $< ================================================ FILE: copyparty/web/Makefile.s1 ================================================ sed -r 's/^\t+//;s/^"([a-zA-Z][a-zA-Z0-9_]*)": /\1:/; /^\/\//d; s/([^\]["'\''],) *\/\/.*/\1/; /^$/d' ================================================ FILE: copyparty/web/a/__init__.py ================================================ ================================================ FILE: copyparty/web/baguettebox.js ================================================ "use strict"; /*! * baguetteBox.js * @author feimosi * @version 1.11.1-mod * @url https://github.com/feimosi/baguetteBox.js */ var J_BBX = 1; window.baguetteBox = (function () { 'use strict'; var options = {}, defaults = { captions: true, buttons: 'auto', noScrollbars: false, bodyClass: 'bbox-open', titleTag: false, async: false, preload: 2, refocus: true, afterShow: null, afterHide: null, duringHide: null, onChange: null, readDirRtl: false, }, overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnZoom, btnVmode, btnReadDir, btnClose, currentGallery = [], currentIndex = 0, isOverlayVisible = false, touch = {}, // start-pos touchFlag = false, // busy scrollCSS = ['', ''], scrollTimer = 0, re_i = APPLE ? /^[^?]+\.(a?png|avif|bmp|gif|hei[cf]s?|jfif|jpe?g|jxl|svg|tiff?|webp)(\?|$)/i : /^[^?]+\.(a?png|avif|bmp|gif|jfif|jpe?g|jxl|svg|tiff?|webp)(\?|$)/i, re_v = /^[^?]+\.(webm|mkv|mp4|m4v|mov)(\?|$)/i, re_cbz = /^[^?]+\.(cbz)(\?|$)/i, anims = ['slideIn', 'fadeIn', 'none'], data = {}, // all galleries imagesElements = [], documentLastFocus = null, isFullscreen = false, vmute = false, vloop = sread('vmode') == 'L', vnext = sread('vmode') == 'C', loopA = null, loopB = null, url_ts = null, un_pp = 0, resume_mp = false; var onFSC = function (e) { isFullscreen = !!document.fullscreenElement; clmod(document.documentElement, 'bb_fsc', isFullscreen); }; var overlayClickHandler = function (e) { if (e.target.id.indexOf('baguette-img') !== -1) hideOverlay(); }; var vtouch = function (e) { var v = vid(), bv = v.getBoundingClientRect(), tp = e.changedTouches[0]; if (bv.bottom - tp.clientY < 90) touchFlag = true; }; var touchstartHandler = function (e) { touch.count = e.touches.length; if (touch.count > 1) touch.multitouch = true; touch.startX = e.changedTouches[0].pageX; touch.startY = e.changedTouches[0].pageY; }; var touchmoveHandler = function (e) { if (touchFlag || touch.multitouch) return; e.preventDefault ? e.preventDefault() : e.returnValue = false; var touchEvent = e.touches[0] || e.changedTouches[0]; if (touchEvent.pageX - touch.startX > 40) { touchFlag = true; showLeftImage(); } else if (touchEvent.pageX - touch.startX < -40) { touchFlag = true; showRightImage(); } else if (touch.startY - touchEvent.pageY > 100) { hideOverlay(); } }; var touchendHandler = function (e) { touch.count--; if (e && e.touches) touch.count = e.touches.length; if (touch.count <= 0) touch.multitouch = false; touchFlag = false; }; var contextmenuHandler = function () { touchendHandler(); }; var overlayWheelHandler = function (e) { if (!options.noScrollbars || anymod(e)) return; ev(e); var x = e.deltaX, y = e.deltaY, d = Math.abs(x) > Math.abs(y) ? x : y; if (e.deltaMode) d *= 10; if (Date.now() - scrollTimer < (Math.abs(d) > 20 ? 100 : 300)) return; scrollTimer = Date.now(); if (d > 0) showNextImageIgnoreReadDir(); else showPreviousImageIgnoreReadDir(); }; var trapFocusInsideOverlay = function (e) { if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) { e.stopPropagation(); btnClose.focus(); } }; function run(selector, userOptions) { buildOverlay(); removeFromCache(selector); return bindImageClickListeners(selector, userOptions); } function bindImageClickListeners(selector, userOptions) { var galleryNodeList = QSA(selector); var selectorData = { galleries: [], nodeList: galleryNodeList }; data[selector] = selectorData; [].forEach.call(galleryNodeList, function (galleryElement) { var tagsNodeList = []; if (galleryElement.tagName === 'A') tagsNodeList = [galleryElement]; else tagsNodeList = galleryElement.getElementsByTagName('a'); if (have_zls) bindCbzClickListeners(tagsNodeList, userOptions); tagsNodeList = [].filter.call(tagsNodeList, function (element) { if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) return re_i.test(element.href) || re_v.test(element.href); }); if (!tagsNodeList.length) return; var gallery = []; [].forEach.call(tagsNodeList, function (imageElement, imageIndex) { var imageElementClickHandler = function (e) { if (ctrl(e) || e && e.shiftKey) return true; e.preventDefault ? e.preventDefault() : e.returnValue = false; prepareOverlay(gallery, userOptions); showOverlay(imageIndex); }; var imageItem = { eventHandler: imageElementClickHandler, imageElement: imageElement, }; bind(imageElement, 'click', imageElementClickHandler); gallery.push(imageItem); }); selectorData.galleries.push(gallery); }); return [selectorData.galleries, options]; } function bindCbzClickListeners(tagsNodeList, userOptions) { var cbzNodes = [].filter.call(tagsNodeList, function (element) { return re_cbz.test(element.href); }); if (!tagsNodeList.length) { return; } [].forEach.call(cbzNodes, function (cbzElement, index) { var gallery = []; var eventHandler = function (e) { if (ctrl(e) || e && e.shiftKey) return true; e.preventDefault ? e.preventDefault() : e.returnValue = false; fillCbzGallery(gallery, cbzElement, eventHandler).then(function () { prepareOverlay(gallery, userOptions); showOverlay(0); } ).catch(function (reason) { console.error("cbz-ded", reason); var t; try { t = uricom_dec(cbzElement.href.split('/').pop()); } catch (ex) { } var msg = "Could not browse " + (t ? t : 'archive'); try { msg += "\n\n" + reason.message; } catch (ex) { } toast.err(20, msg, 'cbz-ded'); }); } bind(cbzElement, "click", eventHandler); }) } function fillCbzGallery(gallery, cbzElement, eventHandler) { if (gallery.length !== 0) { return Promise.resolve(); } var href = cbzElement.href; var zlsHref = href + (href.indexOf("?") === -1 ? "?" : "&") + "zls"; return fetch(zlsHref) .then(function (response) { if (response.ok) { return response.json(); } else { throw new Error("Archive is invalid"); } }) .then(function (fileList) { var imagesList = fileList.map(function (file) { return file["fn"]; }).filter(function (file) { return re_i.test(file); }).sort(); if (imagesList.length === 0) { throw new Error("Archive does not contain any images"); } imagesList.forEach(function (imageName, index) { var imageHref = href + (href.indexOf("?") === -1 ? "?" : "&") + "zget=" + encodeURIComponent(imageName); var galleryItem = { href: imageHref, imageElement: cbzElement, eventHandler: eventHandler, }; gallery.push(galleryItem); }); }); } function clearCachedData() { for (var selector in data) if (data.hasOwnProperty(selector)) removeFromCache(selector); } function removeFromCache(selector) { if (!data.hasOwnProperty(selector)) return; var galleries = data[selector].galleries; [].forEach.call(galleries, function (gallery) { [].forEach.call(gallery, function (imageItem) { unbind(imageItem.imageElement, 'click', imageItem.eventHandler); }); if (currentGallery === gallery) currentGallery = []; }); delete data[selector]; } function buildOverlay() { overlay = ebi('bbox-overlay'); if (!overlay) { var ctr = mknod('div'); ctr.innerHTML = ( '' ); overlay = ctr.firstChild; QS('body').appendChild(overlay); tt.att(overlay); } slider = ebi('bbox-slider'); btnPrev = ebi('bbox-prev'); btnNext = ebi('bbox-next'); btnHelp = ebi('bbox-help'); btnAnim = ebi('bbox-anim'); btnReadDir = ebi('bbox-readdir'); btnRotL = ebi('bbox-rotl'); btnRotR = ebi('bbox-rotr'); btnSel = ebi('bbox-tsel'); btnFull = ebi('bbox-full'); btnZoom = ebi('bbzoom'); btnVmode = ebi('bbox-vmode'); btnClose = ebi('bbox-close'); bcfg_bind(options, 'bbzoom', 'bbzoom', false, setzoom); setzoom(); } function halp() { if (ebi('bbox-halp')) return; var list = [ ['# hotkey', '# operation'], ['escape', 'close'], ['left, J', 'previous file'], ['right, L', 'next file'], ['home', 'first file'], ['end', 'last file'], ['R', 'rotate (shift=ccw)'], ['F', 'toggle fullscreen'], ['Z', 'toggle zoom/stretch'], ['S', 'toggle file selection'], ['space, P, K', 'video: play / pause'], ['U', 'video: seek 10sec back'], ['O', 'video: seek 10sec ahead'], ['0..9', 'video: seek 0%..90%'], ['M', 'video: toggle mute'], ['V', 'video: toggle loop'], ['C', 'video: toggle auto-next'], ['[, ]', 'video: loop start / end'], ], d = mknod('table', 'bbox-halp'), html = ['']; for (var a = 0; a < list.length; a++) html.push('' + list[a][0] + '' + list[a][1] + ''); html.push('tap middle of img to hide btns'); html.push('tap left/right sides for prev/next'); d.innerHTML = html.join('\n') + ''; d.onclick = function () { overlay.removeChild(d); }; overlay.appendChild(d); } function keyDownHandler(e) { if (modal.busy) return; if (anymod(e, true)) return; var k = (e.key || e.code) + '', v = vid(); if (k.startsWith('Key')) k = k.slice(3); else if (k.startsWith('Digit')) k = k.slice(5); var kl = k.toLowerCase(); if (k == '?') return halp(); if (k == "[" || k == "BracketLeft") setloop(1); else if (k == "]" || k == "BracketRight") setloop(2); else if (e.shiftKey && kl != "r") return; else if (k == "ArrowLeft" || k == "Left" || kl == "j") showLeftImage(); else if (k == "ArrowRight" || k == "Right" || kl == "l") showRightImage(); else if (k == "Escape" || k == "Esc") hideOverlay(); else if (k == "Home") showFirstImage(e); else if (k == "End") showLastImage(e); else if (k == "Space" || k == "Spacebar" || kl == " " || kl == "p" || kl == "k") playpause(); else if (kl == "u" || kl == "o") relseek(kl == "u" ? -10 : 10); else if (v && /^[0-9]$/.test(k) && isNum(v.duration)) v.currentTime = v.duration * parseInt(k) * 0.1; else if (kl == "m" && v) { v.muted = vmute = !vmute; mp_ctl(); } else if (kl == "v" && v) { vloop = !vloop; vnext = vnext && !vloop; setVmode(); } else if (kl == "c" && v) { vnext = !vnext; vloop = vloop && !vnext; setVmode(); } else if (kl == "f") tglfull(); else if (kl == "z") btnZoom.click(); else if (kl == "s") tglsel(); else if (kl == "r") rotn(e.shiftKey ? -1 : 1); else if (kl == "y") dlpic(); else return; return ev(e); } function anim() { var i = (anims.indexOf(options.animation) + 1) % anims.length, o = options; swrite('ganim', anims[i]); options = {}; setOptions(o); if (tt.en) tt.show.call(this); } function toggleReadDir() { var o = options, next = options.readDirRtl ? "ltr" : "rtl"; swrite('greaddir', next); slider.className = "no-transition"; options = {}; setOptions(o); updateOffset(true); window.getComputedStyle(slider).opacity; // force a restyle slider.className = ""; if (tt.en) tt.show.call(this); } function setVmode() { var v = vid(); ebi('bbox-vmode').style.display = v ? '' : 'none'; if (!v) return; var msg = 'When video ends, ', tts = '', lbl; if (vloop) { lbl = 'Loop'; msg += 'repeat it'; tts = '$NHotkey: V'; } else if (vnext) { lbl = 'Cont'; msg += 'continue to next'; tts = '$NHotkey: C'; } else { lbl = 'Stop'; msg += 'just stop' } btnVmode.setAttribute('aria-label', msg); btnVmode.setAttribute('tt', msg + tts); btnVmode.textContent = lbl; swrite('vmode', lbl[0]); v.loop = vloop if (vloop && v.paused) v.play(); } function tglVmode() { if (vloop) { vnext = true; vloop = false; } else if (vnext) vnext = false; else vloop = true; setVmode(); if (tt.en) tt.show.call(this); } function findfile() { var thumb = currentGallery[currentIndex].imageElement, name = vsplit(thumb.href)[1].split('?')[0], files = msel.getall(); for (var a = 0; a < files.length; a++) if (vsplit(files[a].vp)[1] == name) return [name, a, files, ebi(files[a].id)]; } function tglfull() { try { if (isFullscreen) document.exitFullscreen(); else ebi('bbox-overlay').requestFullscreen(); } catch (ex) { if (IPHONE) alert('sorry, apple decided to make this impossible on iphones (should work on ipad tho)'); else alert(ex); } } function setzoom() { var sel = clgot(btnZoom, 'on') clmod(ebi('bbox-overlay'), 'fill', sel); btnState(btnZoom, sel); } function tglsel() { var o = findfile()[3]; clmod(o.closest('tr'), 'sel', 't'); msel.selui(); selbg(); } function dlpic() { var url = addq(findfile()[3].href, 'cache'); dl_file(url); } function selbg() { var img = vidimg(), thumb = currentGallery[currentIndex].imageElement, name = vsplit(thumb.href)[1].split('?')[0], files = msel.getsel(), sel = false; for (var a = 0; a < files.length; a++) if (vsplit(files[a].vp)[1] == name) sel = true; ebi('bbox-overlay').style.background = sel ? 'rgba(153,34,85,0.7)' : ''; img.style.borderRadius = sel ? '1em' : ''; btnState(btnSel, sel); } function btnState(btn, sel) { btn.style.color = sel ? '#fff' : ''; btn.style.background = sel ? '#d48' : ''; btn.style.textShadow = sel ? '1px 1px 0 #b38' : ''; btn.style.boxShadow = sel ? '.15em .15em 0 #502' : ''; } function keyUpHandler(e) { if (anymod(e)) return; var k = (e.key || e.code) + ''; if (k == "Space" || k == "Spacebar" || k == " ") { un_pp = Date.now(); return ev(e); } } var passiveSupp = false; try { var opts = { get passive() { passiveSupp = true; return false; } }; window.addEventListener('test', null, opts); window.removeEventListener('test', null, opts); } catch (ex) { passiveSupp = false; } var passiveEvent = passiveSupp ? { passive: false } : null; var nonPassiveEvent = passiveSupp ? { passive: true } : null; function bindEvents() { bind(document, 'keydown', keyDownHandler); bind(document, 'keyup', keyUpHandler); bind(document, 'fullscreenchange', onFSC); bind(overlay, 'click', overlayClickHandler); bind(overlay, 'wheel', overlayWheelHandler); bind(btnPrev, 'click', showLeftImage); bind(btnNext, 'click', showRightImage); bind(btnClose, 'click', hideOverlay); bind(btnVmode, 'click', tglVmode); bind(btnHelp, 'click', halp); bind(btnAnim, 'click', anim); bind(btnReadDir, 'click', toggleReadDir); bind(btnRotL, 'click', rotl); bind(btnRotR, 'click', rotr); bind(btnSel, 'click', tglsel); bind(btnFull, 'click', tglfull); bind(slider, 'contextmenu', contextmenuHandler); bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent); bind(overlay, 'touchmove', touchmoveHandler, passiveEvent); bind(overlay, 'touchend', touchendHandler); bind(document, 'focus', trapFocusInsideOverlay, true); } function unbindEvents() { unbind(document, 'keydown', keyDownHandler); unbind(document, 'keyup', keyUpHandler); unbind(document, 'fullscreenchange', onFSC); unbind(overlay, 'click', overlayClickHandler); unbind(overlay, 'wheel', overlayWheelHandler); unbind(btnPrev, 'click', showLeftImage); unbind(btnNext, 'click', showRightImage); unbind(btnClose, 'click', hideOverlay); unbind(btnVmode, 'click', tglVmode); unbind(btnHelp, 'click', halp); unbind(btnAnim, 'click', anim); unbind(btnReadDir, 'click', toggleReadDir); unbind(btnRotL, 'click', rotl); unbind(btnRotR, 'click', rotr); unbind(btnSel, 'click', tglsel); unbind(btnFull, 'click', tglfull); unbind(slider, 'contextmenu', contextmenuHandler); unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent); unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent); unbind(overlay, 'touchend', touchendHandler); unbind(document, 'focus', trapFocusInsideOverlay, true); timer.rm(rotn); } function prepareOverlay(gallery, userOptions) { if (currentGallery === gallery) return; currentGallery = gallery; setOptions(userOptions); slider.innerHTML = ''; imagesElements.length = 0; var imagesFiguresIds = []; var imagesCaptionsIds = []; for (var i = 0, fullImage; i < gallery.length; i++) { fullImage = mknod('div', 'baguette-img-' + i); fullImage.className = 'full-image'; imagesElements.push(fullImage); imagesFiguresIds.push('bbox-figure-' + i); imagesCaptionsIds.push('bbox-figcaption-' + i); slider.appendChild(imagesElements[i]); } overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' ')); overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' ')); } function setOptions(newOptions) { if (!newOptions) newOptions = {}; for (var item in defaults) { options[item] = defaults[item]; if (typeof newOptions[item] !== 'undefined') options[item] = newOptions[item]; } var an = options.animation = sread('ganim', anims) || anims[ANIM ? 0 : 2]; btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)]; btnAnim.setAttribute('tt', 'animation: ' + an); options.readDirRtl = sread('greaddir') === "rtl"; var msg; if (options.readDirRtl) { btnReadDir.innerText = "rtl"; msg = "browse from right to left"; slider.style.display = "flex"; slider.style.flexDirection = "row-reverse"; } else { btnReadDir.innerText = "ltr"; msg = "browse from left to right"; slider.style.flexDirection = ""; slider.style.display = "block"; } btnReadDir.setAttribute("tt", msg); btnReadDir.setAttribute("aria-label", msg); slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .3s ease' : options.animation === 'slideIn' ? '' : 'none'); if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) options.buttons = false; btnPrev.style.display = btnNext.style.display = (options.buttons ? '' : 'none'); } function showOverlay(chosenImageIndex) { if (options.noScrollbars) { var a = document.documentElement.style.overflowY, b = document.body.style.overflowY; if (a != 'hidden' || b != 'scroll') scrollCSS = [a, b]; document.documentElement.style.overflowY = 'hidden'; document.body.style.overflowY = 'scroll'; } if (overlay.style.display === 'block') return; bindEvents(); currentIndex = chosenImageIndex; touch = { count: 0, startX: null, startY: null }; loadImage(currentIndex, function () { preloadNext(currentIndex); preloadPrev(currentIndex); }); show_buttons(0); updateOffset(); overlay.style.display = 'block'; // Fade in overlay setTimeout(function () { clmod(overlay, 'visible', 1); if (options.bodyClass && document.body.classList) document.body.classList.add(options.bodyClass); if (options.afterShow) options.afterShow(); }, 50); if (options.onChange && !url_ts) options.onChange.call(currentGallery, currentIndex, imagesElements.length); url_ts = null; documentLastFocus = document.activeElement; btnClose.focus(); isOverlayVisible = true; } function hideOverlay(e, dtor) { ev(e); playvid(false); removeFromCache('#files'); if (options.noScrollbars) { document.documentElement.style.overflowY = scrollCSS[0]; document.body.style.overflowY = scrollCSS[1]; } try { if (document.fullscreenElement) document.exitFullscreen(); } catch (ex) { } isFullscreen = false; if (toast.tag == 'bb-ded') toast.hide(); if (dtor || overlay.style.display === 'none') return; if (options.duringHide) options.duringHide(); sethash(''); unbindEvents(); // Fade out and hide the overlay clmod(overlay, 'visible'); setTimeout(function () { overlay.style.display = 'none'; if (options.bodyClass && document.body.classList) document.body.classList.remove(options.bodyClass); qsr('#bbox-halp'); if (options.afterHide) options.afterHide(); options.refocus && documentLastFocus && documentLastFocus.focus(); isOverlayVisible = false; unvid(); unfig(); }, 250); } function unvid(keep) { var vids = QSA('#bbox-overlay video'); for (var a = vids.length - 1; a >= 0; a--) { var v = vids[a]; if (v == keep) continue; unbind(v, 'touchstart', vtouch, nonPassiveEvent); unbind(v, 'error', lerr); v.src = ''; v.load(); var p = v.parentNode; p.removeChild(v); p.parentNode.removeChild(p); } } function unfig(keep) { var figs = QSA('#bbox-overlay figure'), npre = options.preload || 0, k = []; if (keep === undefined) keep = -9; for (var a = keep - npre; a <= keep + npre; a++) k.push('bbox-figure-' + a); for (var a = figs.length - 1; a >= 0; a--) { var f = figs[a]; if (!has(k, f.getAttribute('id'))) f.parentNode.removeChild(f); } } function lerr() { var t; try { t = this.getAttribute('src'); t = uricom_dec(t.split('/').pop().split('?')[0]); } catch (ex) { } t = 'Failed to open ' + (t?t:'file'); console.log('bb-ded', t); t += '\n\nEither the file is corrupt, or your browser does not understand the file format or codec'; try { t += "\n\nerr#" + this.error.code + ", " + this.error.message; } catch (ex) { } this.ded = esc(t); if (this === vidimg()) toast.err(20, this.ded, 'bb-ded'); } function loadImage(index, callback) { var imageContainer = imagesElements[index]; var galleryItem = currentGallery[index]; if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') return; // out-of-bounds or gallery dirty if (imageContainer.querySelector('img, video')) // was loaded, cb and bail return callback ? callback() : null; // maybe unloaded video while (imageContainer.firstChild) imageContainer.removeChild(imageContainer.firstChild); var imageElement = galleryItem.imageElement, imageSrc = galleryItem.href || imageElement.href, is_vid = re_v.test(imageSrc), thumbnailElement = imageElement.querySelector('img, video'), imageCaption = typeof options.captions === 'function' ? options.captions.call(currentGallery, imageElement, index) : imageElement.getAttribute('data-caption') || imageElement.title; imageSrc = addq(imageSrc, 'cache'); if (is_vid && index != currentIndex) return; // no preload var figure = mknod('figure', 'bbox-figure-' + index); figure.innerHTML = '
    ' + '
    ' + '
    ' + '
    '; if (options.captions && imageCaption) { var figcaption = mknod('figcaption', 'bbox-figcaption-' + index); figcaption.innerHTML = imageCaption; figure.appendChild(figcaption); } imageContainer.appendChild(figure); var image = mknod(is_vid ? 'video' : 'img'); clmod(imageContainer, 'vid', is_vid); bind(image, 'error', lerr); bind(image, is_vid ? 'loadedmetadata' : 'load', function () { // Remove loader element qsr('#baguette-img-' + index + ' .bbox-spinner'); if (!options.async && callback) callback(); }); image.setAttribute('src', imageSrc); if (is_vid) { image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1); image.setAttribute('controls', 'controls'); image.setAttribute('playsinline', '1'); // ios ignores poster image.onended = vidEnd; image.onplay = image.onpause = ppHandler; } image.alt = thumbnailElement ? thumbnailElement.alt || '' : ''; if (options.titleTag && imageCaption) image.title = imageCaption; figure.appendChild(image); if (is_vid && window.afilt) afilt.apply(undefined, image); if (options.async && callback) callback(); } function ppHandler() { var now = Date.now(); if (now - un_pp < 50) { un_pp = 0; return playpause(); // browser undid space hotkey } show_buttons(this.paused ? 0 : 1); } function showRightImage(e) { ev(e); var dir = options.readDirRtl ? -1 : 1; return show(currentIndex + dir); } function showLeftImage(e) { ev(e); var dir = options.readDirRtl ? 1 : -1; return show(currentIndex + dir); } function showNextImageIgnoreReadDir(e) { ev(e); return show(currentIndex + 1); } function showPreviousImageIgnoreReadDir(e) { ev(e); return show(currentIndex - 1); } function showFirstImage(e) { if (e) e.preventDefault(); return show(0); } function showLastImage(e) { if (e) e.preventDefault(); return show(currentGallery.length - 1); } function show(index, gallery) { gallery = gallery || currentGallery; if (!isOverlayVisible && index >= 0 && index < gallery.length) { prepareOverlay(gallery, options); showOverlay(index); return true; } if (index < 0) return bounceAnimation(options.readDirRtl ? 'right' : 'left'); if (index >= imagesElements.length) return bounceAnimation(options.readDirRtl ? 'left' : 'right'); var orot; try { orot = vidimg().getAttribute('rot'); vid().pause(); } catch (ex) { } currentIndex = index; loadImage(currentIndex, function () { preloadNext(currentIndex); preloadPrev(currentIndex); }); updateOffset(); var im = vidimg(); if (im && im.ded) toast.err(20, im.ded, 'bb-ded'); else if (toast.tag == 'bb-ded') toast.hide(); if (orot && im.getAttribute('rot') === null) rotn(orot / 90, 1); if (options.animation == 'none') unvid(vid()); else setTimeout(function () { unvid(vid()); }, 100); unfig(index); if (options.onChange) options.onChange.call(currentGallery, currentIndex, imagesElements.length); return true; } var prev_cw = 0, prev_ch = 0, unrot_timer = null; function rotn(n, asap) { var el = vidimg(), orot = parseInt(el.getAttribute('rot') || 0), frot = orot + (n || 0) * 90; if (!frot && !orot) return; // reflow noop var co = ebi('bbox-overlay'), cw = co.clientWidth, ch = co.clientHeight; if (!n && prev_cw === cw && prev_ch === ch) return; // reflow noop clmod(el, 'asap', asap); prev_cw = cw; prev_ch = ch; var rot = frot, iw = el.naturalWidth || el.videoWidth, ih = el.naturalHeight || el.videoHeight, magic = 4, // idk, works in enough browsers dl = el.closest('div').querySelector('figcaption a'), vw = cw, vh = ch - dl.offsetHeight + magic, pmag = Math.min(vw / ih, vh / iw), wmag = Math.min(vw / iw, vh / ih); if (!options.bbzoom) { pmag = Math.min(1, pmag); wmag = Math.min(1, wmag); } while (rot < 0) rot += 360; while (rot >= 360) rot -= 360; var q = rot == 90 || rot == 270 ? 1 : 0, mag = q ? pmag : wmag; el.style.cssText = 'max-width:none; max-height:none; position:absolute; display:block; margin:0'; if (!orot) { el.style.width = iw * wmag + 'px'; el.style.height = ih * wmag + 'px'; el.style.left = (vw - iw * wmag) / 2 + 'px'; el.style.top = (vh - ih * wmag) / 2 - magic + 'px'; q = el.offsetHeight; } el.style.width = iw * mag + 'px'; el.style.height = ih * mag + 'px'; el.style.left = (vw - iw * mag) / 2 + 'px'; el.style.top = (vh - ih * mag) / 2 - magic + 'px'; el.style.transform = 'rotate(' + frot + 'deg)'; el.setAttribute('rot', frot); timer.add(rotn); if (!rot) { clearTimeout(unrot_timer); unrot_timer = setTimeout(unrot, 300); } } function rotl() { rotn(-1); } function rotr() { rotn(1); } function unrot() { var el = vidimg(), orot = el.getAttribute('rot'), rot = parseInt(orot || 0); while (rot < 0) rot += 360; while (rot >= 360) rot -= 360; if (rot || orot === null) return; clmod(el, 'nt', 1); el.setAttribute('rot', 0); el.removeAttribute("style"); rot = el.offsetHeight; clmod(el, 'nt'); timer.rm(rotn); } function vid() { if (currentIndex >= imagesElements.length) return; return imagesElements[currentIndex].querySelector('video'); } function vidimg() { if (currentIndex >= imagesElements.length) return; return imagesElements[currentIndex].querySelector('img, video'); } function playvid(play) { if (!play) { timer.rm(loopchk); loopA = loopB = null; } var v = vid(); if (!v) return; v[play ? 'play' : 'pause'](); if (play && loopA !== null && v.currentTime < loopA) v.currentTime = loopA; } function playpause() { var v = vid(); if (v) v[v.paused ? "play" : "pause"](); } function relseek(sec) { var v = vid(), t = v && v.currentTime; if (isNum(t)) v.currentTime = t + sec; } function vidEnd() { if (this == vid() && vnext) showNextImageIgnoreReadDir(); } function setloop(side) { var v = vid(); if (!v) return; var t = v.currentTime; if (side == 1) loopA = t; if (side == 2) loopB = t; if (side) toast.inf(5, 'Loop' + (side == 1 ? 'A' : 'B') + ': ' + f2f(t, 2)); if (loopB !== null) { timer.add(loopchk); sethash(location.hash.slice(1).split('&')[0] + '&t=' + (loopA || 0) + '-' + loopB); } } function loopchk() { if (loopB === null) return; var v = vid(); if (!v || v.paused || v.currentTime < loopB) return; v.currentTime = loopA || 0; } function urltime(txt) { url_ts = txt; } function mp_ctl() { var v = vid(); if (!vmute && v && mp.au && !mp.au.paused) { mp.fade_out(); resume_mp = true; } else if (resume_mp && (vmute || !v) && mp.au && mp.au.paused) { mp.fade_in(); resume_mp = false; } } function show_buttons(v) { clmod(ebi('bbox-btns'), 'off', v); clmod(btnPrev, 'off', v); clmod(btnNext, 'off', v); } function bounceAnimation(direction) { slider.className = options.animation == 'slideIn' ? 'bounce-from-' + direction : 'eog'; setTimeout(function () { slider.className = ''; }, 300); return false; } function updateOffset(noTransition) { var dir = options.readDirRtl ? 1 : -1, offset = dir * currentIndex * 100 + '%', xform = slider.style.perspective !== undefined; if (options.animation === 'fadeIn' && !noTransition) { slider.style.opacity = 0; setTimeout(function () { xform ? slider.style.transform = 'translate3d(' + offset + ',0,0)' : slider.style.left = offset; slider.style.opacity = 1; }, 100); } else { xform ? slider.style.transform = 'translate3d(' + offset + ',0,0)' : slider.style.left = offset; } playvid(false); var v = vid(); if (v) { playvid(true); v.muted = vmute; v.loop = vloop; if (url_ts) { var seek = ('' + url_ts).split('-'); v.currentTime = seek[0]; if (seek.length > 1) { loopA = parseFloat(seek[0]); loopB = parseFloat(seek[1]); setloop(); } } bind(v, 'touchstart', vtouch, nonPassiveEvent); } selbg(); mp_ctl(); setVmode(); var el = vidimg(); if (el.getAttribute('rot')) timer.add(rotn); else timer.rm(rotn); var ctime = 0; el.onclick = v ? null : function (e) { var rc = e.target.getBoundingClientRect(), x = e.clientX - rc.left, fx = x / (rc.right - rc.left); if (fx < 0.3) return showLeftImage(); if (fx > 0.7) return showRightImage(); show_buttons('t'); if (Date.now() - ctime <= 500 && !IPHONE) tglfull(); ctime = Date.now(); }; var prev = QS('.full-image.vis'); if (prev) clmod(prev, 'vis'); clmod(el.closest('div'), 'vis', 1); } function preloadNext(index) { if (index - currentIndex >= options.preload) return; loadImage(index + 1, function () { preloadNext(index + 1); }); } function preloadPrev(index) { if (currentIndex - index >= options.preload) return; loadImage(index - 1, function () { preloadPrev(index - 1); }); } function bind(element, event, callback, options) { element.addEventListener(event, callback, options); } function unbind(element, event, callback, options) { element.removeEventListener(event, callback, options); } function destroyPlugin() { hideOverlay(undefined, true); unbindEvents(); clearCachedData(); document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay')); data = {}; currentGallery = []; currentIndex = 0; } return { run: run, show: show, showNext: showRightImage, showPrevious: showLeftImage, relseek: relseek, urltime: urltime, playpause: playpause, hide: hideOverlay, destroy: destroyPlugin }; })(); J_BBX = 2; ================================================ FILE: copyparty/web/browser.css ================================================ :root { color-scheme: dark; --grid-sz: 10em; --grid-ln: 3; --nav-sz: 16em; --sbw: 0.5em; --sbh: 0.5em; --fg: #ccc; --fg-max: #fff; --fg2-max: #fff; --fg-weak: #bbb; --bg-u6: #4c4c4c; --bg-u5: #444; --bg-u4: #383838; --bg-u3: #333; --bg-u2: #2b2b2b; --bg-u1: #282828; --bg: #222; --bgg: var(--bg); --bg-d1: #1c1c1c; --bg-d2: #181818; --bg-d3: #111; --bg-max: #000; --tab-alt: #f5a; --row-alt: #282828; --scroll: #eb0; --sel-fg: var(--bg-d1); --sel-bg: var(--fg); --a: #fc5; --a-b: #c90; --a-hil: #fd9; --a-dark: #e70; --a-gray: #666; --btn-fg: var(--a); --btn-bg: rgba(128,128,128,0.15); --btn-h-fg: var(--a-hil); --btn-h-bg: #805; --btn-1-fg: #400; --btn-1-bg: var(--a); --btn-h-bs: var(--btn-bs); --btn-h-bb: var(--btn-bb); --btn-1-bs: var(--btn-bs); --btn-1-bb: var(--btn-bb); --btn-1h-fg: var(--btn-1-fg); --btn-1h-bg: #fe8; --btn-1h-bs: var(--btn-1-bs); --btn-1h-bb: var(--btn-1-bb); --chk-fg: var(--tab-alt); --txt-sh: var(--bg-d2); --txt-bg: var(--btn-bg); --op-aa-fg: var(--a); --op-aa-bg: var(--bg-d2); --op-a-sh: rgba(0,0,0,0.5); --u2-btn-b1: #999; --u2-sbtn-b1: #999; --u2-txt-bg: var(--bg-u5); --u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1)); --u2-tab-b1: rgba(128,128,128,0.8); --u2-tab-1-fg: #fd7; --u2-tab-1-bg: linear-gradient(to bottom, #353, var(--bg) 80%); --u2-tab-1-b1: #7c5; --u2-tab-1-b2: #583; --u2-tab-1-sh: #280; --u2-b-fg: #fff; --u2-b1-bg: #c38; --u2-b2-bg: #d80; --u2-inf-bg: #07a; --u2-inf-b1: #0be; --u2-ok-bg: #380; --u2-ok-b1: #8e4; --u2-err-bg: #900; --u2-err-b1: #d06; --ud-b1: #888; --sort-1: #fb0; --sort-2: #d09; --sz-b: #aaa; --sz-k: #4ff; --sz-m: var(--tab-alt); --sz-g: var(--a); --sz-t: var(--sz-g); --sz-p: var(--sz-t); --srv-1: #aaa; --srv-2: #a73; --srv-3: #f4c; --srv-3b: rgba(255,68,204,0.6); --tree-bg: #2b2b2b; --g-play-bg: #750; --g-play-b1: #c90; --g-play-b2: #da4; --g-play-sh: #b83; --g-sel-fg: #fff; --g-sel-bg: #925; --g-sel-b1: #e39; --g-sel-sh: #b36; --g-fsel-bg: #d39; --g-fsel-b1: #f4a; --g-fsel-ts: #804; --g-dfg: var(--srv-3); --g-fg: var(--a-hil); --g-bg: var(--bg-u2); --g-b1: var(--bg-u4); --g-b2: var(--bg-u5); --g-g1: var(--bg-u2); --g-g2: var(--bg-u5); --g-f-bg: var(--bg-u4); --g-f-b1: var(--bg-u5); --g-f-fg: var(--a-hil); --g-sh: rgba(0,0,0,0.3); --f-sh1: 0.33; --f-sh2: 0.02; --f-sh3: 0.2; --f-h-b1: rgba(128,128,128,0.7); --f-play-bg: #fc5; --f-play-fg: #000; --f-sel-sh: #fc0; --f-gray: #999; --fm-off: #f6c; --mp-sh: var(--bg-d3); --mp-b-bg: rgba(0,0,0,0.2); --err-fg: #fff; --err-bg: #a20; --err-b1: #f00; --err-ts: #500; } html.y { color-scheme: light; --fg: #222; --fg-max: #000; --fg-weak: #555; --bg-d3: #fff; --bg-d2: #fff; --bg-d1: #fff; --bg: #eaeaea; --bg-u1: #fff; --bg-u2: #f7f7f7; --bg-u3: #eaeaea; --bg-u4: #fff; --bg-u5: #ccc; --bg-u6: #ddd; --bg-max: #fff; --tab-alt: #c07; --row-alt: #f2f2f2; --scroll: #490; --a: #06a; --a-b: #08b; --a-hil: #058; --a-gray: #bbb; --a-dark: #c0f; --btn-fg: #555; --btn-h-fg: #222; --btn-h-bg: #caf; --btn-1-fg: #fff; --btn-1-bg: #4a0; --btn-1h-bg: #5c0; --chk-fg: var(--fg); --txt-sh: #aaa; --txt-bg: rgba(255,255,255,0.6); --op-a-sh: #fff; --u2-txt-bg: var(--bg-max); --u2-tab-1-sh: #0ad; --u2-tab-1-b1: #09c; --u2-tab-1-b2: #05a; --u2-tab-1-fg: var(--fg-max); --u2-tab-1-bg: inherit; --ud-b1: #bbb; --sort-1: #059; --sort-2: #f5d; --sz-b: #777; --sz-k: #380; --srv-1: #555; --srv-2: #c83; --srv-3: #c0a; --tree-bg: #fff; --g-fg: var(--a); --g-bg: var(--bg-u2); --g-b1: var(--bg-u6); --g-b2: var(--bg-u6); --g-g1: var(--bg-u2); --g-g2: var(--bg-u5); --g-f-bg: var(--bg-u4); --g-f-b1: var(--bg-u5); --g-sh: rgba(0,0,0,0.07); --f-sh1: 0.3; --f-sh2: 0.5; --f-sh3: 0.02; --f-sel-sh: #e80; --fm-off: #c4a; --mp-sh: #bbb; --mp-b-bg: transparent; text-shadow: none; } html.a { --op-aa-sh: 0 0 .2em var(--bg-d3) inset; --btn-bs: 0 0 .2em var(--bg-d3); } html.az { --btn-1-bs: 0 0 .1em var(--fg) inset; } html.ay { --op-aa-sh: 0 .1em .2em #ccc; --op-aa-bg: var(--bg-max); } html.b { --btn-bs: 0 .05em 0 var(--bg-d3) inset; --btn-1-bs: 0 .05em 0 var(--btn-1h-bg) inset; --tree-bg: var(--bg); --g-bg: var(--bg); --g-b1: var(--bg); --g-b2: var(--bg); --g-g1: var(--bg); --g-sh: rgba(0,0,0,0); --op-aa-bg: rgba(255,255,255,0.06); --u2-sbtn-b1: #fc0; --u2-txt-bg: transparent; --u2-tab-1-sh: var(--bg); --u2-b1-bg: rgba(128,128,128,0.15); --u2-b2-bg: var(--u2-b1-bg); --f-sh1: 0.1; --mp-b-bg: transparent; } html.bz { --fg: #cce; --fg-weak: #bbd; --bg-u5: #3b3f58; --bg-u4: #1e2130; --bg-u3: #1e2130; --bg-u1: #1e2130; --bg: #11121d; --bg-d1: #232536; --bg-d2: #34384e; --bg-d3: #34384e; --row-alt: #181a27; --a-b: #fb4; --btn-bg: #202231; --btn-h-bg: #2d2f45; --btn-1-bg: #eb6; --btn-1-fg: #000; --btn-1h-fg: #000; --btn-1h-bg: #ff9; --txt-sh: a; --u2-tab-b1: var(--bg-u5); --u2-tab-1-fg: var(--fg-max); --u2-tab-1-bg: var(--bg); --srv-1: #79b; --g-sel-bg: #ba2959; --g-fsel-bg: #e6336e; --f-h-b1: #34384e; --mp-sh: #11121d; /*--mp-b-bg: #2c3044;*/ --f-play-bg: var(--btn-1-bg); } html.by { --bg: #f2f2f2; --row-alt: #f9f9f9; --scroll: var(--a); --btn-1-bg: #07a; --btn-1h-bg: var(--a-hil); --op-aa-bg: #fff; --u2-sbtn-b1: #c70; --u2-tab-1-b1: #999; --u2-tab-1-b2: #aaa; --u2-b-fg: #444; } html.c { font-weight: bold; --fg: #fff; --fg-weak: #cef; --bg-u5: #409; --bg-u2: linear-gradient(-35deg, #fd7233, #cd27a0, #5d47a5 49.5%, #16e9fb 50%, #3b6cc8 50.4%, #0e51ac); --bg: #37235d; --bg-u3: #407; --a: #f9dc22; --a-gray: #0ae; --tab-alt: #6ef; --row-alt: #47237d; --scroll: #ff0; --btn-fg: #fff; --btn-bg: #9019bf; --btn-h-bg: #a039ff; --chk-fg: #d90; --op-aa-bg: #f9dd22; --srv-1: #ea0; --mp-b-bg: transparent; } html.cz { --bgg: var(--bg-u2); --sel-bg: var(--bg-u5); --sel-fg: var(--fg); --btn-bb: .2em solid #709; --btn-bs: 0 .1em .6em rgba(255,0,185,0.5); --btn-1-bb: .2em solid #e90; --btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9); --sz-b: #ddd; --sz-k: #c9f; --srv-3: #fff; --u2-tab-b1: var(--bg-d3); --u2-tab-1-bg: a; } html.cy { --fg: #fff; --fg-weak: #fff; --bg: #ff0; --bg-u2: #f00; --bg-u3: #f00; --bg-u5: #999; --bg-d3: #f77; --bg-d2: #ff0; --sel-bg: #f77; --a: #fff; --a-hil: #fff; --a-h-bg: #000; --tab-alt: #f00; --row-alt: #fff; --scroll: #fff; --btn-bg: #000; --btn-fg: #ff0; --btn-h-fg: #fff; --btn-1-bg: #ff0; --btn-1-fg: #000; --btn-bs: 0 .25em 0 #f00; --chk-fg: #fd0; --txt-bg: #000; --srv-1: #f00; --srv-3: #fff; --op-aa-bg: #fff; --u2-b1-bg: #f00; --u2-b2-bg: #f00; --g-sel-fg: #fff; --g-sel-bg: #aaa; --g-fsel-bg: #aaa; } html.dz { --fg: #4d4; --fg-weak: #2a2; --bg-u6: #020; --bg-u5: #050; --bg-u4: #020; --bg-u3: #020; --bg-u2: #020; --bg-u1: #020; --bg: #010; --bg-d1: #000; --bg-d2: #020; --bg-d3: #000; --tab-alt: #6f6; --row-alt: #030; --scroll: #0f0; --a: #9f9; --a-b: #cfc; --a-hil: #cfc; --a-dark: #afa; --a-gray: #2a2; --btn-bg: rgba(64,128,64,0.15); --btn-h-bg: #050; --btn-1-fg: #000; --btn-1-bg: #4f4; --btn-1h-bg: #3f3; --btn-bs: 0 0 0 .1em #080 inset; --btn-1-bs: a; --u2-btn-b1: var(--fg-weak); --u2-sbtn-b1: var(--fg-weak); --u2-tab-b1: var(--fg-weak); --u2-tab-1-fg: #fff; --u2-tab-1-bg: linear-gradient(to bottom, #151, var(--bg) 80%); --u2-b1-bg: #3a3; --u2-b2-bg: #3a3; --sort-1: #fff; --sort-2: #3f3; --srv-1: #3e3; --srv-2: #1a1; --srv-3: #0f0; --srv-3b: #070; --tree-bg: #010; --g-sel-b1: #c37; --g-sel-sh: #b36; --g-fsel-b1: #d48; --f-h-b1: #3b3; text-shadow: none; font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } html.dy { --fg: #000; --fg-max: #000; --fg-weak: #000; --bg-d3: #fff; --bg-d2: #fff; --bg-d1: #fff; --bg: #fff; --bg-u1: #fff; --bg-u2: #fff; --bg-u3: #fff; --bg-u4: #fff; --bg-u5: #fff; --bg-u6: #fff; --bg-max: #fff; --tab-alt: #000; --row-alt: #eee; --scroll: #000; --a: #000; --a-b: #000; --a-hil: #000; --a-gray: #bbb; --a-dark: #000; --btn-fg: #000; --btn-h-fg: #000; --btn-h-bg: #fff; --btn-1-fg: #fff; --btn-1-bg: #000; --btn-1h-bg: #555; --chk-fg: a; --txt-sh: a; --txt-bg: a; --op-a-sh: a; --u2-txt-bg: a; --u2-tab-1-sh: a; --u2-tab-1-b1: a; --u2-tab-1-b2: a; --u2-tab-1-fg: a; --u2-tab-1-bg: a; --u2-b1-bg: #000; --u2-b2-bg: #000; --ud-b1: a; --sort-1: a; --sort-2: a; --srv-1: a; --srv-2: a; --srv-3: a; --srv-3b: a; --tree-bg: #fff; --g-sel-bg: #000; --g-fsel-bg: #444; --g-fsel-ts: #000; --g-fg: a; --g-bg: a; --g-b1: a; --g-b2: a; --g-g1: a; --g-g2: a; --g-f-bg: a; --g-f-b1: a; --g-sh: a; --f-sh1: a; --f-sh2: a; --f-sh3: a; --f-sel-sh: #000; --fm-off: a; --mp-sh: a; --mp-b-bg: #fff; } * { line-height: 1.2em; } ::selection { color: var(--sel-fg); background: var(--sel-bg); text-shadow: none; } html,body,tr,th,td,#files,a,#blogout { color: inherit; background: none; font-weight: inherit; font-size: inherit; padding: 0; border: none; } html { color: var(--fg); background: var(--bgg); font-family: sans-serif; font-family: var(--font-main), sans-serif; text-shadow: 1px 1px 0px var(--bg-max); } html, body { margin: 0; padding: 0; } pre, code, tt, #doc, #doc>code { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } .ayjump { position: fixed; overflow: hidden; width: 0; height: 0; left: -10em; color: var(--bg); } html .ayjump:focus { z-index: 80386; color: #fff; color: var(--a-hil); background: #069; background: var(--bg-u2); border: .2em solid var(--a); box-shadow: none; outline: none; width: auto; height: auto; top: .5em; left: .5em; padding: .5em .7em; } #path, #path * { font-size: 1em; } #path { color: var(--fg); text-shadow: 1px 1px 0 var(--bg-max); font-weight: normal; display: inline-block; padding: .35em .5em .2em .5em; margin: 1.3em 0 -.2em 0; font-size: 1.4em; } html.y #path { text-shadow: none; } #path #entree { margin-left: -.7em; } #files { z-index: 1; top: -.3em; border-spacing: 0; position: relative; } #files tbody a { display: block; padding: .5em 0; margin: -.3em 0; scroll-margin-top: 45vh; } #files tr { scroll-margin-top: 25vh; scroll-margin-bottom: 20vh; } #files tbody div a { color: var(--tab-alt); } a, #blogout, #files tbody div a:last-child { color: var(--a); padding: .2em; text-decoration: none; } #blogout { margin: -.2em; } #blogout:hover, a:hover { color: var(--a-hil); background: var(--a-h-bg); } #files a:hover { color: var(--fg-max); background: var(--bg-d3); text-decoration: underline; } #files thead th { position: sticky; top: -1px; } #files thead a { color: var(--f-gray); font-weight: normal; } .s0:after, .s1:after { content: '⌄'; margin-left: -.15em; } .s0r:after, .s1r:after { content: '⌃'; margin-left: -.15em; } .s0:after, .s0r:after { color: var(--sort-1); } .s1:after, .s1r:after { color: var(--sort-2); } #files thead th:after { margin-right: -.5em; } #files tbody tr:hover td, #files tbody tr:hover td+td { background: var(--bg-d1); box-shadow: 0 1px 0 var(--bg-u5) inset, 0 -1px 0 var(--bg-u5) inset; } #files thead th { padding: .3em; background: var(--bg); border-bottom: 1px solid var(--f-h-b1); cursor: pointer; } html.y #files thead th { box-shadow: 0 1px 0 rgba(0,0,0,0.12); } html #files.hhpick thead th { color: #f7d; background: #000; box-shadow: .1em .2em 0 #f6c inset, -.1em -.1em 0 #f6c inset; } #files td { margin: 0; padding: .3em .5em; background: var(--bg); max-width: var(--file-td-w); word-wrap: break-word; overflow: hidden; } #files tr.fade a { color: #999; color: rgba(255, 255, 255, 0.4); font-style: italic; } html.y #files tr.fade a { color: #999; color: rgba(0, 0, 0, 0.4); } #files tr:nth-child(2n) td { background: var(--row-alt); } #files td+td { box-shadow: 1px 0 0 0 rgba(128,128,128,var(--f-sh1)) inset, 0 1px 0 rgba(255,255,255,var(--f-sh2)) inset, 0 -1px 0 rgba(255,255,255,var(--f-sh2)) inset; } #files tr:nth-child(2n+1) td+td { box-shadow: 1px 0 0 0 rgba(128,128,128,var(--f-sh1)) inset, 0 1px 0 rgba(0,0,0,var(--f-sh3)) inset, 0 -1px 0 rgba(0,0,0,var(--f-sh3)) inset; } #files td:first-child { border-radius: .25em 0 0 .25em; white-space: nowrap; } #files td:last-child { border-radius: 0 .25em .25em 0; } #files tbody td:nth-child(3) { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; text-align: right; padding-right: 1em; white-space: nowrap; } #files tbody td:first-child { color: var(--f-gray); text-align: center; } #files tbody tr td:last-child { white-space: nowrap; } #files span.fsz_B { color: var(--sz-b); } #files span.fsz_K { color: var(--sz-k); } #files span.fsz_M { color: var(--sz-m); } #files span.fsz_G { color: var(--sz-g); } #files span.fsz_T { color: var(--sz-t); } #files span.fsz_P { color: var(--sz-p); } html.y #files span.fsz_G, html.y #files span.fsz_T, html.y #files span.fsz_P { font-weight: bold } #files thead th[style] { width: auto !important; } #files .srch_hdr a { display: inline; } #path a { padding: 0 .35em; position: relative; z-index: 1; /* ie: */ border-bottom: .1em solid #777\9; margin-right: 1em\9; } #path a:first-child { padding-left: .8em; } #path i { width: 1.05em; height: 1.05em; margin: -.5em .15em -.15em -.7em; display: inline-block; border: 1px solid rgba(255,224,192,0.3); border-width: .05em .05em 0 0; transform: rotate(45deg); background: linear-gradient(45deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.25) 75%, rgba(0,0,0,0.35)); } html.y #path i { background: none; border-color: rgba(0,0,0,0.2); border-width: .1em .1em 0 0; } #path a:hover { color: var(--fg-max); background: linear-gradient(90deg, rgba(0,0,0,0), rgba(0,0,0,0.2), rgba(0,0,0,0)); } html.y #path a:hover { background: none; } .logue { padding: .2em 0; position: relative; z-index: 1; } .logue.hidden, .logue:empty { display: none; } .logue.raw { white-space: pre; font-family: 'scp', 'consolas', monospace; font-family: var(--font-mono), 'scp', 'consolas', monospace; } #doc>iframe, .logue>iframe { background: var(--bgg); border: 1px solid var(--bgg); border-width: 0 .3em 0 .3em; border-radius: .5em; visibility: hidden; margin: 0 -.3em; width: 100%; height: 0; } #doc>iframe.focus, .logue>iframe.focus { box-shadow: 0 0 .1em .1em var(--a); } #pro.logue>iframe { height: 100vh; } #pro.logue { margin-bottom: .8em; } #epi.logue { margin: .8em 0; } #epi.logue.mdo:before { content: 'README.md'; text-align: center; display: block; margin-top: -1.5em; } #epi.logue.mdo { border-top: 1px solid var(--bg-u5); margin-top: 2.5em; } .mdo>h1:first-child, .mdo>h2:first-child, .mdo>h3:first-child { margin-top: 1.5rem; } .mdo { max-width: 52em; } .mdo.sb, .logue.mdo>iframe { max-width: 54em; } .mdo, .mdo * { line-height: 1.5em; } #srv_info, #srv_info2, #acc_info { color: var(--srv-2); background: var(--bg); white-space: nowrap; } #srv_info, #acc_info { position: absolute; font-size: .8em; top: .5em; } #srv_info { left: 2em; padding-right: .5em; } #acc_info, #srv_info span, #srv_info2 span { color: var(--srv-1); } #srv_info2 a { padding: 0; } #srv_info2 { display: none; } #acc_info { right: 2em; } #acc_info > span:not([id]) { color: var(--srv-1); margin-right: .6em; } #acc_info span.warn { color: var(--srv-3); border-bottom: 1px solid var(--srv-3b); } #flogout { display: inline; } html.dz #flogout { margin-left: 1em; } #goh+span { color: var(--bg-u5); padding-left: .5em; margin-left: .5em; border-left: .2em solid var(--bg-u5); } #repl { padding: .33em; } #files a.doc { color: var(--a-gray); } #files a.doc.bri { color: var(--tab-alt); } #files a.play { color: var(--a-dark); padding: .3em; margin: -.3em; } #files tbody tr.play td, #files tbody tr.play td+td, #files tbody tr.play div a { background: #fc0; background: var(--f-play-bg); color: var(--f-play-fg); text-shadow: none; } #files tbody tr.play a { color: inherit; } #files tbody tr.play a:hover { color: var(--btn-1h-fg); background: var(--btn-1h-bg); box-shadow: var(--btn-1h-bs); border-bottom: var(--btn-1h-bb); } #ggrid { margin: -.2em -.5em; } #ggrid>a>span { overflow: hidden; display: block; display: -webkit-box; line-clamp: var(--grid-ln); -webkit-line-clamp: var(--grid-ln); -webkit-box-orient: vertical; padding-top: .3em; } #ggrid>a { display: inline-block; width: 10em; width: var(--grid-sz); vertical-align: top; overflow-wrap: break-word; border-radius: .3em; padding: .3em; margin: .5em; color: var(--g-fg); background: var(--g-bg); border: 1px solid var(--g-b1); border-top: 1px solid var(--g-b2); box-shadow: 0 .1em .3em var(--g-sh); } #ggrid>a:focus, #ggrid>a:hover { color: var(--g-f-fg); background: var(--g-f-bg); border-color: var(--g-f-b1); box-shadow: 0 .1em .3em var(--g-sh); } #ggrid>a img { border-radius: .2em; max-width: 10em; max-width: var(--grid-sz); max-height: 8em; max-height: calc(var(--grid-sz)/1.25); margin: 0 auto; display: block; } #ggrid.nocrop>a img { max-height: 20em; max-height: calc(var(--grid-sz)*2); } #ggrid>a.dir:before { content: '📂'; } #ggrid>a.dir>span { color: var(--g-dfg); } #ggrid>a.au:before { content: '▶'; } #ggrid>a:before { display: block; position: absolute; padding: .3em 0; margin: -.4em; text-shadow: 0 0 .1em var(--bg-max); background: linear-gradient(135deg,rgba(255,255,255,0) 50%,rgba(255,255,255,0.2)); border-radius: .3em; font-size: 2em; transition: font-size .15s, margin .15s; } #ggrid>a:focus:before, #ggrid>a:hover:before { font-size: 2.5em; margin: -.2em; } #ggrid>a[tt] { background: linear-gradient(135deg, var(--g-g1) 95%, var(--g-g2) 95%); } #ggrid>a[tt]:focus, #ggrid>a[tt]:hover { background: var(--g-f-bg); } #ggrid>a.play, #ggrid>a[tt].play { color: var(--g-sel-fg); background: #fc0; background: var(--g-play-bg); border-color: var(--g-play-b1); border-top: 1px solid var(--g-play-b2); box-shadow: 0 .1em 1.2em var(--g-play-sh); } #files tbody tr.sel td, #files tbody tr.sel span, #ggrid>a.sel, #ggrid>a[tt].sel { color: var(--g-sel-fg); background: #f3c; background: var(--g-sel-bg); border-color: var(--g-sel-b1); } #ggrid>a.sel>span { color: var(--g-sel-fg); } #ggrid>a.sel, #ggrid>a[tt].sel { border-top: 1px solid var(--g-fsel-b1); box-shadow: 0 .1em 1.2em var(--g-sel-sh); transition: all 0.2s cubic-bezier(.2, 2.2, .5, 1); /* https://cubic-bezier.com/#.4,2,.7,1 */ } #files tbody tr.sel:hover td, #files tbody tr.sel:focus td, #ggrid>a.sel:hover, #ggrid>a.sel:focus { color: var(--g-sel-fg); background: var(--g-fsel-bg); border-color: var(--g-fsel-b1); text-shadow: 1px 1px 0 var(--g-fsel-ts); } #ggrid>a.sel img, #ggrid>a.play img { opacity: .7; filter: contrast(130%) brightness(107%); } #ggrid>a.sel img { box-shadow: 0 0 1em var(--g-sel-sh); } #ggrid>a.play img { box-shadow: 0 0 1em var(--g-play-sh); } #ggrid a { scroll-margin-top: 25vh; scroll-margin-bottom: 20vh; } #files tr.sel a, #files tr.sel a.play { color: var(--fg2-max); } #files tr.sel a:hover { color: var(--fg-max); text-shadow: none; } #files tr.sel a.play.act { text-shadow: 0 0 1px var(--fg2-max); } #files tr:focus { outline: none; position: relative; } #files tr:focus td+td { background: var(--bg-d3); box-shadow: 0 .2em 0 var(--f-sel-sh), 0 -.2em 0 var(--f-sel-sh); } #files tr:focus:not(.play):not(.sel) td:first-child { background: var(--bg-d3); box-shadow: -.2em .2em 0 var(--f-sel-sh), -.2em -.2em 0 var(--f-sel-sh); } #player { display: none; } #widget { position: fixed; font-size: 1.4em; left: 0; right: 0; bottom: -6em; height: 6em; width: 100%; z-index: 3; touch-action: none; } #widget.anim { transition: bottom 0.15s; } #widget.open { box-shadow: 0 0 1em rgba(0,48,64,0.2); bottom: 0; } html.y #widget.open { border-top: .2em solid var(--bg-u2); } #widgeti { position: relative; z-index: 10; width: 100%; height: 100%; } #fshr, #wtgrid, #wtico { position: relative; font-size: .9em; top: -.04em; } #wtgrid { font-size: .75em; padding: .1em; top: -.12em; } #wtico { cursor: pointer; } @keyframes spin { 100% {transform: rotate(360deg)} } @keyframes fadein { 0% {opacity: 0} 100% {opacity: 1} } #wtoggle { position: absolute; white-space: nowrap; top: -1em; right: 0; height: 1em; font-size: 2em; line-height: 1em; text-align: center; text-shadow: none; box-shadow: 0 0 .5em var(--mp-sh); border-radius: .3em 0 0 0; padding: 0 0 0 .1em; color: var(--fg-max); } #wtoggle, #widgeti { background: #fff; background: var(--bg-u3); } #wfs, #wfm, #wzip, #wnp, #wm3u { display: none; } #wfs, #wzip, #wnp, #wm3u { margin-right: .2em; padding-right: .2em; border: 1px solid var(--bg-u5); border-width: 0 .1em 0 0; } #wzip1 { margin-right: .2em; } #wfm.act+#wzip1+#wzip, #wfm.act+#wzip1+#wzip+#wnp { margin-left: .2em; padding-left: .2em; border-left-width: .1em; } #wfs.act, #wfm.act { display: inline-block; } #wtoggle, #wtoggle * { line-height: 1em; } #wtoggle.sel #wzip, #wtoggle.m3u #wm3u, #wtoggle.np #wnp { display: inline-block; } #wtoggle.sel #wzip1, #wtoggle.sel.np #wnp { display: none; } #wfm a, #wnp a, #wm3u a, #zip1, #wzip a { font-size: .5em; padding: 0 .3em; margin: -.3em .1em; position: relative; display: inline-block; } #zip1 { font-size: .38em; } #wm3u a { margin: -.2em .1em; font-size: .45em; } #wfs { font-size: .36em; text-align: right; line-height: 1.3em; padding: 0 .3em 0 0; border-width: 0 .25em 0 0; } #wfm span, #wm3u span, #zip1 span, #wnp span { font-size: .6em; display: block; } #zip1 span { font-size: .9em; } #wnp span { font-size: .7em; } #wm3u span { font-size: .77em; padding-top: .2em; } #wfm a:not(.en) { opacity: .3; color: var(--fm-off); } #wfm a.hide { display: none; } #files tbody tr.fcut td, #ggrid>a.fcut { animation: fcut .5s ease-out; } @keyframes fcut { 0% {opacity:0} 100% {opacity:1} } #ggrid>a.glow { animation: gexit .6s ease-out; } @keyframes gexit { 0% {box-shadow: 0 0 0 2em var(--a)} 100% {box-shadow: 0 0 0em 0em var(--a)} } #wzip a { font-size: .4em; margin: -.3em .1em; } #wtoggle.sel .l1 { top: -.6em; padding: .4em .3em; } #barpos, #barbuf { position: absolute; bottom: 1em; left: 1em; height: 2em; border-radius: 9em; width: calc(100% - 2em); } #barbuf { background: var(--mp-b-bg); z-index: 21; } #barpos { box-shadow: -.03em -.03em .7em rgba(0,0,0,0.5) inset; z-index: 22; } #pctl { position: absolute; top: .5em; left: 1em; } #pctl a { display: inline-block; font-size: 1.25em; width: 1.3em; height: 1.2em; text-align: center; border-radius: .3em; } #pvol { position: absolute; top: .7em; right: 1em; height: 1.6em; border-radius: 9em; max-width: 12em; width: calc(100% - 10.5em); background: rgba(0,0,0,0.2); } #widget.cmp { height: 1.6em; bottom: -1.6em; } #widget.cmp.open { bottom: 0; } #widget.cmp #wtoggle { font-size: 1.2em; } #widget.cmp #fshr, #widget.cmp #wtgrid { display: none; } #widget.cmp #pctl { top: 0; left: 0; font-size: .75em; } #widget.cmp #pctl a { margin: 0; } #widget.cmp #barpos, #widget.cmp #barbuf { height: 1.6em; width: calc(100% - 11em); border-radius: 0; left: 5em; top: 0; } #widget.cmp #pvol { top: 0; right: 0; max-width: 5.8em; border-radius: 0; } .opview { display: none; } .opview.act { display: block; } #ops a { color: var(--a); text-shadow: 1px 1px 1px var(--op-a-sh); font-size: 1.5em; padding: .25em .4em; margin: 0; } #ops a.act { color: #fff; color: var(--op-aa-fg); background: #000; background: var(--op-aa-bg); border-radius: 0 0 .2em .2em; border-bottom: .3em solid var(--a-b); box-shadow: var(--op-aa-sh); } #ops a svg { width: 1.75em; height: 1.75em; margin: -.5em -.3em; } html.y #ops svg circle { stroke: black; } #ops { padding: .3em .6em; white-space: nowrap; } #noie { color: #b60; margin: 0 0 0 .5em; } .opbox { padding: .5em; border-radius: 0 .3em .3em 0; border-width: 1px 1px 1px 0; max-width: 41em; max-width: min(41em, calc(100% - 2.6em)); } .opbox input { position: relative; margin: .5em; } #op_cfg input[type=text] { top: -.3em; } .opview select, .opview input[type=text] { color: var(--fg); background: var(--txt-bg); border: none; box-shadow: 0 0 2px var(--txt-sh); border-bottom: 1px solid #999; border-color: var(--a); border-radius: .2em; padding: .2em .3em; } .opview select { padding: .3em; margin: .2em .4em; background: var(--bg-u3); } .opview input.err { color: var(--err-fg); background: var(--err-bg); border-color: var(--err-b1); box-shadow: 0 0 .7em var(--err-b1); text-shadow: 1px 1px 0 var(--err-ts); outline: none; } input[type="checkbox"]+label { color: var(--chk-fg); } input[type="radio"]:checked+label, input[type="checkbox"]:checked+label { color: #0e0; color: var(--btn-1-bg); } input[type="checkbox"]:checked+label { box-shadow: var(--btn-1-bs); border-bottom: var(--btn-1-bb); } html.dz input { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } .opwide div>span>input+label { padding: .3em 0 .3em .3em; margin: 0 0 0 -.3em; cursor: pointer; } .opview input.i { width: calc(100% - 16.2em); } input.drc_v, input.eq_gain, input.ssconf_v { width: 3em; text-align: center; margin: 0 .6em; } #audio_drc table, #audio_eq table { border-collapse: collapse; } #audio_drc td, #audio_eq td, #audio_ss td { text-align: center; } #audio_eq a.eq_step { font-size: 1.5em; display: block; padding: 0; } #au_ss, #au_drc, #au_eq { display: block; margin-top: .5em; padding: 1.3em .3em; } #au_ss, #au_drc { padding: .4em .3em; } #au_ss.on { width: 3em; height: 1em; overflow: hidden; } #ico1 { cursor: pointer; } #srch_form { padding: 0 .5em .5em 0; } #srch_form table { display: inline-block; } #srch_form td { padding: .6em .6em; } #srch_form td:first-child { width: 3em; padding-right: .2em; text-align: right; } #srch_form:not(.tags) #tsrch_tags, #srch_form:not(.tags) #tsrch_adv { display: none; } #op_search input { margin: .1em 0 0 0; } #srch_q { white-space: pre; color: var(--a-b); min-height: 1em; margin: .2em 0 -1em 1.6em; } #srch_q.err { color: var(--srv-3); } #tq_raw { width: calc(100% - 2em); margin: .3em 0 0 1.4em; } @media (max-width: 130em) { #srch_form.tags #tq_raw { width: calc(100% - 34em) } } @media (max-width: 95em) { #srch_form.tags #tq_raw { width: calc(100% - 2em) } } #tq_raw td+td { width: 100%; } #op_search #q_raw { width: 100%; display: block; } #files td div span { color: var(--fg-max); padding: 0 .4em; font-weight: bold; font-style: italic; } #files td div a:hover { background: var(--bg-u5); color: var(--fg-max); } #files td div a { display: inline-block; white-space: nowrap; } #files td div a:last-child { width: 100%; } #files td div { border-collapse: collapse; width: 100%; } #wrap { margin: 1.8em 1.5em 0 1.5em; min-height: 70vh; padding-bottom: 7em; } #tree { display: none; position: absolute; left: 0; bottom: 0; top: 7em; overflow-x: hidden; overflow-y: auto; -ms-scroll-chaining: none; overscroll-behavior-y: none; box-shadow: 0 0 1em var(--bg-d2), 0 -1px 0 rgba(128,128,128,0.3); } #tree, html { scrollbar-color: var(--scroll) var(--bg-u3); } #treeh { position: sticky; z-index: 1; top: 0; height: 2.2em; line-height: 2.2em; background: var(--tree-bg); border-bottom: 1px solid var(--bg-d3); overflow: hidden; } #treepar { z-index: 1; position: fixed; background: #fff; background: var(--tree-bg); left: -.96em; width: calc(.3em + var(--nav-sz) - var(--sbw)); border-bottom: 1px solid var(--bg-u5); overflow: hidden; border-right: .5em solid #999\9; } #treepar.off { display: none; } .np_open #thx_ff { padding: 4.5em 0; /* widget */ } #tree::-webkit-scrollbar-track, #tree::-webkit-scrollbar { background: var(--bg-u3); } #tree::-webkit-scrollbar-thumb { background: var(--scroll); } #tree:hover { z-index: 2; } #treeul { position: relative; left: -2.2em; width: calc(100% + 2em); } .btn { color: var(--btn-fg); background: #eee; background: var(--btn-bg); box-shadow: var(--btn-bs); border-bottom: var(--btn-bb); border-radius: .3em; padding: .2em .4em; font-size: 1.2em; margin: .2em; display: inline-block; white-space: pre; position: relative; top: -.12em; } html.c .btn, html.a .btn { border-radius: .2em; } html.dz .btn { font-size: 1em; } .btn:hover { color: var(--btn-h-fg); background: var(--btn-h-bg); box-shadow: var(--btn-h-bs); border-bottom: var(--btn-h-bb); } .tgl.btn.on { background: #000; background: var(--btn-1-bg); color: #fff; color: var(--btn-1-fg); text-shadow: none; box-shadow: var(--btn-1-bs); border-bottom: var(--btn-1-bb); } .tgl.btn.on:hover { color: var(--btn-1h-fg); background: var(--btn-1h-bg); box-shadow: var(--btn-1h-bs); border-bottom: var(--btn-1h-bb); } #detree { padding: .3em .5em; font-size: 1.5em; line-height: 1.5em; } #tree ul, #tree li { padding: 0; margin: 0; } #tree ul { border-left: .2em solid var(--bg-u5); margin-left: .44em; } #tree li { margin-left: .6em; list-style: none; border-top: 1px solid var(--bg-u5); } #tree li.offline>a:first-child:before { content: '❌'; position: absolute; margin-left: -.25em; z-index: 3; } #tree ul a.sel { background: #000; background: var(--bg-d3); box-shadow: -.8em 0 0 var(--g-sel-b1) inset; color: #fff; color: var(--fg-max); } #tree ul a.hl { color: #fff; color: var(--btn-1-fg); background: #000; background: var(--btn-1-bg); text-shadow: none; } #tree ul a.ld::before { font-weight: bold; font-family: sans-serif; display: inline-block; text-align: center; width: 1em; margin: 0 .3em 0 -1.3em; color: var(--fg-max); opacity: 0; content: '◠'; animation: .5s linear infinite forwards spin, ease .25s 1 forwards fadein; } #tree ul a.par { color: var(--fg-max); } #tree ul a { border-radius: .3em; display: inline-block; } .ntree a+a { width: calc(100% - 2.2em); line-height: 1em; } #tree.nowrap li { min-height: 1.4em; white-space: nowrap; } #tree.nowrap .ntree a+a:hover { background: rgba(16, 16, 16, 0.67); min-width: calc(var(--nav-sz) - 2em); width: auto; } html.y #tree.nowrap .ntree a+a:hover { background: rgba(255, 255, 255, 0.67); color: var(--fg-max); } #docul a:hover, #tree .ntree a+a:hover { background: var(--bg-d2); color: var(--fg-max); } .ntree a:first-child { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; font-size: 1.2em; line-height: 0; } .dumb_loader_thing { display: block; margin: 1em .3em 1em 1em; padding: 0 1.2em 0 0; font-size: 4em; min-width: 1em; min-height: 1em; opacity: 0; animation: 1s linear .15s infinite forwards spin, .2s ease .15s 1 forwards fadein; position: fixed; top: .3em; z-index: 9; } #dlt_t { left: 0; } #dlt_f { right: .5em; } #files .cfg { display: none; font-size: 2em; white-space: nowrap; } #files th:hover .cfg { display: block; width: 1em; border-radius: .2em; margin: -1.2em auto 0 auto; background: var(--bg-u5); } #files th span { position: relative; white-space: nowrap; } #files>thead>tr>th.min, #files td.min { display: none; } #files td:nth-child(2n) { color: var(--tab-alt); } #plazy { width: 1px; height: 1px; overflow: hidden; white-space: nowrap; } #blazy { text-align: center; font-size: 1.2em; margin: 1em 0; } #blazy code, #blazy a { font-size: 1.1em; padding: 0 .2em; } .opwide, #op_unpost, #srch_form { max-width: none; margin-right: 1.5em; } .opwide>div { display: inline-block; vertical-align: top; border-left: .4em solid var(--bg-u5); margin: .7em 0 .7em .5em; padding-left: .5em; } .opwide>div>h3 { color: var(--fg-weak); margin: 0 .4em; padding: 0; } #op_cfg>div>div>span { display: inline-block; padding: .2em .4em; } .opbox h3 { margin: .8em 0 0 .6em; padding: 0; } #thumbs, #au_prescan, #au_fullpre, #au_os_seek, #au_osd_cv, #u2tdate { opacity: .3; } #griden.on+#thumbs, #au_preload.on+#au_prescan, #au_preload.on+#au_prescan+#au_fullpre, #au_os_ctl.on+#au_os_seek, #au_os_ctl.on+#au_os_seek+#au_osd_cv, #u2turbo.on+#u2tdate { opacity: 1; } #wraptree.on+#hovertree { display: none; } .ghead { background: #fff; background: var(--bg-u2); border-radius: .3em; padding: .2em .5em; line-height: 2.3em; margin-bottom: 1.5em; } #hdoc, #ghead { position: sticky; top: -.3em; z-index: 2; } .ghead .btn { position: relative; top: 0; } .ghead>span { white-space: pre; padding-left: .3em; } #tailbtns { display: none; } #taildoc.on+#tailbtns { display: inherit; display: unset; } #op_unpost { padding: 1em; } #op_unpost td { padding: .2em .4em; } #op_unpost a { margin: 0; padding: 0; } #unpost td:nth-child(3), #unpost td:nth-child(4) { text-align: right; } #shui, #rui { background: #fff; background: var(--bg); position: fixed; top: 0; left: 0; width: calc(100% - 2em); height: auto; overflow: auto; max-height: calc(100% - 2em); border-bottom: .5em solid var(--f-gray); box-shadow: 0 0 5em rgba(0,0,0,0.8); padding: 1em; z-index: 765; } #shui div+div, #rui div+div { margin-top: 1em; } #shui table, #rui table { width: 100%; border-collapse: collapse; } #shui button { margin: 0 1em 0 0; } #shui .btn { font-size: 1em; } #shui td { padding: .8em 0; } #shui td+td, #rui td+td { padding: .2em 0 .2em .5em; } #rn_vadv input { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } #shui td+td, #rui td+td, #shui td input[type="text"], #rui td input[type="text"] { width: 100%; } #rui #rn_n_d, #rui #rn_n_s, #shui td.exs input[type="text"] { width: 3em; } #rn_f.m td:first-child { white-space: nowrap; } #rn_f.m td+td { width: 50%; } #rn_f .err td, #rn_f .err input[readonly], #rui .ng input[readonly] { color: var(--err-fg); background: var(--err-bg); } #rui input[readonly] { color: var(--fg-max); background: var(--bg-u5); border: 1px solid rgba(255,255,255,0.2); padding: .2em .25em; } #rui h1 { margin: 0 0 .3em 0; padding: 0; font-size: 1.5em; } #fs_abrt { margin-top: 1em; text-shadow: 0; box-shadow: 1px 1px 0 var(--bg-d3); } #doc { overflow: visible; background: #fff; background: var(--bg); margin: -1em 0 .5em 0; padding: 1em 0 1em 0; border-radius: .3em; } #doc.wrap { white-space: pre-wrap; } html.y #doc { box-shadow: 0 0 .3em var(--bg-u5); background: #f7f7f7; } #docul { position: relative; } #docul li.bn { text-align: center; padding: .5em; } #docul li.bn span { font-weight: bold; color: var(--fg-max); } #doc.prism { padding-left: 3em; } #doc>code { background: none; box-shadow: none; z-index: 1; } #doc.mdo { white-space: normal; font-family: sans-serif; font-family: var(--font-main), sans-serif; } #doc.prism * { line-height: 1.5em; } #doc .line-highlight { border-radius: .3em; box-shadow: 0 0 .5em rgba(128,128,128,0.2); background: linear-gradient(90deg, var(--bg-d3), var(--bg)); } html.y #doc .line-highlight { background: linear-gradient(90deg, var(--bg-max), var(--bg)); } #docul li { margin: 0; } #tree #docul li+li a { display: block; } #seldoc.sel { color: var(--fg2-max); background: #f0f; background: var(--g-sel-b1); } #pvol, #barbuf, #barpos, a.btn, #u2btn, #u2conf label, #rui label, #modal-ok, #modal-ng, #ops, #ico1 { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } #hkhelp { background: #fff; background: var(--bg); } #hkhelp table { margin: 2em 2em 0 2em; float: left; } #hkhelp th { border-bottom: 1px solid var(--bg-u5); background: var(--bg-u1); font-weight: bold; text-align: right; } #hkhelp tr+tr th { border-top: 1.5em solid var(--bg); } #hkhelp td { padding: .2em .3em; } #hkhelp td:first-child { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } #hkhelp b { text-shadow: 1px 0 0 var(--fg), -1px 0 0 var(--fg), 0 -1px 0 var(--fg); } html.noscroll, html.noscroll .sbar { scrollbar-width: none; } html.noscroll::-webkit-scrollbar, html.noscroll .sbar::-webkit-scrollbar { display: none; } /* bbox */ #bbox-overlay { display: none; opacity: 0; position: fixed; overflow: hidden; touch-action: pinch-zoom; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; background: rgba(0, 0, 0, 0.8); transition: opacity .3s ease; } #bbox-overlay.visible { opacity: 1; } .full-image { display: inline-block; position: relative; width: 100%; height: 100%; text-align: center; flex: none; } .full-image figure { display: inline; margin: 0; height: 100%; } .full-image img, .full-image video { display: inline-block; outline: none; width: auto; height: auto; max-width: 100%; max-height: 100%; max-height: calc(100% - 1.4em); margin-bottom: 1.4em; vertical-align: middle; transition: transform .23s, left .23s, top .23s, width .23s, height .23s; } .full-image img.asap, .full-image video.asap { transition: none; } #bbox-overlay.fill .full-image img, #bbox-overlay.fill .full-image video { width: 100%; height: 100%; object-fit: contain; } html.bb_fsc .full-image img, html.bb_fsc .full-image video { max-height: 100%; } html.bb_fsc figcaption { display: none; } .full-image img.nt, .full-image video.nt { transition: none; } .full-image.vis img, .full-image.vis video { box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); } .full-image video { background: #222; } .full-image figcaption { display: block; position: fixed; bottom: .1em; width: 100%; text-align: center; white-space: normal; color: var(--fg); z-index: 1; } #bbox-overlay figcaption a { background: rgba(0, 0, 0, 0.6); border-radius: .4em; padding: .3em .6em; } html.y #bbox-overlay figcaption a { color: #0bf; } .full-image:before { content: ""; display: inline-block; height: 50%; width: 1px; margin-right: -1px; } #bbox-slider { position: fixed; left: 0; top: 0; height: 100%; width: 100%; white-space: nowrap; transition: left .2s ease, transform .2s ease; } .bounce-from-right { animation: bounceFromRight .3s ease-out; } .bounce-from-left { animation: bounceFromLeft .3s ease-out; } .eog { animation: eog .2s; } @keyframes bounceFromRight { 0% {margin-left: 0} 50% {margin-left: -30px} 100% {margin-left: 0} } @keyframes bounceFromLeft { 0% {margin-left: 0} 50% {margin-left: 30px} 100% {margin-left: 0} } @keyframes eog { 0% {filter: brightness(1.5)} } #bbox-next, #bbox-prev { top: 50%; top: calc(50% - 30px); width: 44px; height: 60px; transition: background-color .3s ease, color .3s ease, left .3s ease, right .3s ease; } #bbox-btns button { transition: background-color .3s ease, color .3s ease; } #bbox-btns { transition: top .3s ease; } .bbox-btn { position: fixed; } #bbox-next.off { right: -2.6em; } #bbox-prev.off { left: -2.6em; } #bbox-btns.off { top: -2.2em; } #bbox-overlay button { cursor: pointer; outline: none; padding: 0 .3em; margin: 0 .4em; border: 0; border-radius: 15%; background: rgba(50, 50, 50, 0.5); color: rgba(255,255,255,0.7); font-size: 1.4em; line-height: 1.4em; vertical-align: top; font-variant: small-caps; } #bbox-overlay button:focus, #bbox-overlay button:hover { color: rgba(255,255,255,0.9); background: rgba(50, 50, 50, 0.9); } #bbox-next { right: 1%; } #bbox-prev { left: 1%; } #bbox-btns { top: .5em; right: 2%; position: fixed; } #bbox-halp { color: var(--fg-max); background: #fff; background: var(--bg); position: absolute; top: 0; left: 0; z-index: 20; padding: .4em; } #bbox-halp td { padding: .2em .5em; } #bbox-halp td:first-child:not([colspan]) { text-align: right; } .bbox-spinner { width: 40px; height: 40px; display: inline-block; position: absolute; top: 50%; left: 50%; margin-top: -20px; margin-left: -20px; } .bbox-double-bounce1, .bbox-double-bounce2 { width: 100%; height: 100%; border-radius: 50%; background-color: #fff; opacity: .6; position: absolute; top: 0; left: 0; animation: bounce 2s infinite ease-in-out; } .bbox-double-bounce2 { animation-delay: -1s; } @keyframes bounce { 0%, 100% {transform: scale(0)} 50% {transform: scale(1)} } .no-transition { transition: none !important; } /* upload.css */ #op_up2k { padding: 0 1em 1em 1em; } #drops { display: none; z-index: 3; background: rgba(48, 48, 48, 0.7); } #drops.vis, .dropzone { display: block; position: fixed; top: 0; left: 0; right: 0; bottom: 0; } .dropdesc { position: fixed; display: table; left: 10%; width: 78%; height: 26%; margin: 0; font-size: 3em; font-weight: bold; text-shadow: .05em .05em .1em #333; background: rgba(224, 224, 224, 0.2); box-shadow: 0 0 0 #999; border: .5em solid var(--ud-b1); border-radius: .5em; border-width: 1vw; color: #fff; transition: all 0.12s; } .dropdesc.hl.ok { border-color: #fff; box-shadow: 0 0 1em .4em #cf5, 0 0 1em #000 inset; background: rgba(24, 24, 24, 0.7); left: 8%; width: 82%; height: 32%; margin: -3vh 0; } .dropdesc.hl.err { background: rgba(224, 32, 65, 0.2); box-shadow: 0 0 1em .4em #f26; border-color: #fab; } .dropdesc>div { display: table-cell; vertical-align: middle; text-align: center; } .dropdesc>div>div { position: absolute; top: 40%; top: calc(50% - .5em); top: calc(50% - 1.2vw); left: -.8em; transition: top 0.12s; } .dropdesc>div>div+div { left: auto; right: -.8em; } .dropdesc b { font-size: .5em; font-size: 2vw; margin: 0 .8em; margin: 0 1.25vw; transition: font-size 0.12s; } .dropdesc.hl.ok b { border-bottom: .1em solid #fff; font-size: .6em; font-size: 2.5vw; } .dropdesc.hl.ok>div>div { top: calc(50% - 1.7vw); } .dropzone { z-index: 80386; height: 50%; } #up_dz { bottom: 50%; } #srch_dz { top: 50%; } #up_zd { top: 12%; } #srch_zd { bottom: 12%; } .dropdesc span { color: #fff; background: #490; border-bottom: .3em solid #6d2; text-shadow: 1px 1px 1px #000; border-radius: .3em; padding: .4em .8em; font-size: .4em; } .dropdesc.err span { background: #904; border-color: #d26; } #u2form { position: absolute; top: 0; left: 0; width: 2px; height: 2px; overflow: hidden; } #u2form input { background: var(--bg-u5); border: 0px solid var(--bg-u5); } #u2err.err { color: var(--a-dark); padding: .5em; } #u2err.msg { color: var(--fg-weak); padding: .5em; font-size: .9em; } #u2btn { line-height: 1.3em; border: .15em dashed var(--u2-btn-b1); border-radius: .4em; text-align: center; font-size: 1.5em; margin: .5em auto; padding: .8em 0; width: 16em; cursor: pointer; } #op_up2k.srch #u2btn { border-color: var(--u2-sbtn-b1); } #u2conf.ww #u2btn { line-height: 1em; padding: .5em 0; margin: -2em 1em -3em 0; } #u2conf #u2btn { padding: .4em 0; margin: -2em 0; font-size: 1.25em; width: 100%; max-width: 12em; display: inline-block; } #u2conf #u2btn_cw { text-align: right; } #u2bm { display: block; } #u2bm sup { font-weight: bold; } #u2notbtn { display: none; text-align: center; background: var(--bg); padding-top: 1em; } #u2notbtn * { line-height: 1.3em; } #u2mu div { height: 1.2em; overflow: hidden; } #u2tabw { min-height: 0; transition: min-height .2s; margin: 2em 0; } #u2tabw.na>table { display: none; } #u2tab { table-layout: fixed; border-collapse: collapse; width: calc(100% - 2em); max-width: 100em; margin: 0 auto; } #op_up2k.srch #u2tabf { max-width: none; } #u2tab td { word-wrap: break-word; border: 1px solid rgba(128,128,128,0.8); border-width: 0 0px 1px 0; padding: .2em .3em; } #u2tab td:nth-child(2) { width: 5em; white-space: nowrap; } #u2tab td:nth-child(3) { width: 40%; } #u2tab.up.ok td:nth-child(3), #u2tab.up.bz td:nth-child(3), #u2tab.up.q td:nth-child(3) { width: 18em; } @media (max-width: 65em) { #u2tab { font-size: .9em; } } @media (max-width: 50em) { #u2tab.up.ok td:nth-child(3), #u2tab.up.bz td:nth-child(3), #u2tab.up.q td:nth-child(3) { width: 16em; } } #op_up2k.srch td.prog { font-family: sans-serif; font-family: var(--font-main), sans-serif; font-size: 1em; width: auto; } #u2tab tbody tr:hover td { background: var(--bg-u2); } #u2etas { padding: .2em .5em; width: 17em; cursor: pointer; text-align: center; white-space: nowrap; display: inline-block; font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } #u2etas.o { width: 20em; } #u2etas .o { display: none; } #u2etas.o .o { display: inherit; display: unset; } #u2etaw { width: 18em; font-size: .94em; margin: 1.8em auto .5em auto; } #u2etas.o #u2etaw { width: 21em; } #u2cards { padding: 1em 1em .42em 1em; margin: 0 auto; white-space: nowrap; text-align: center; overflow: hidden; min-width: 24em; } #u2cards.w { width: 48em; text-align: left; } #u2cards.ww { display: inline-block; } #u2etaw.w { width: 55em; text-align: right; margin: 2em auto -2.7em auto; } #u2etaw.ww { margin: -1em 2em 1em 1em; } #u2cards a { padding: .2em 1em; background: var(--u2-tab-bg); border: 1px solid #999; border-color: var(--u2-tab-b1); border-width: 0 0 1px 0; } #u2cards a:first-child { border-radius: .4em 0 0 0; } #u2cards a:last-child { border-radius: 0 .4em 0 0; } #u2cards a.act { padding-bottom: .5em; border-width: 1px 1px .1em 1px; border-radius: .3em .3em 0 0; margin-left: -1px; background: var(--u2-tab-1-bg); box-shadow: 0 -.17em .67em var(--u2-tab-1-sh); border-color: var(--u2-tab-1-b1) var(--u2-tab-1-b2) var(--bg) var(--u2-tab-1-b2); color: var(--u2-tab-1-fg); position: relative; } #u2cards span { color: var(--fg-max); font-family: 'scp', monospace; font-family: var(--font-mono), 'scp', monospace; } #u2cards > a:nth-child(4) > span { display: inline-block; text-align: center; min-width: 1.3em; } #u2conf { margin: 1em auto; width: 30em; } #u2conf.w { width: 51em; } #u2conf.ww { width: 82em; } #u2conf.ww #u2c3w { width: 29em; } #u2conf.ww #u2c3w.s { width: 39em; } #u2conf .c, #u2conf .c * { text-align: center; line-height: 1em; margin: 0; padding: 0; border: none; } #u2conf .txtbox { width: 3em; color: var(--fg-max); background: var(--u2-txt-bg); border: 1px solid #777; font-size: 1.2em; padding: .15em 0; height: 1.05em; } #u2conf .txtbox.err { color: var(--err-fg); background: var(--err-bg); } #u2conf a.b { color: var(--u2-b-fg); background: var(--u2-b1-bg); text-decoration: none; border-radius: .1em; font-size: 1.5em; padding: .1em 0; margin: 0 -1px; width: 1.5em; height: 1em; display: inline-block; position: relative; bottom: -0.08em; } #u2conf input+a.b { background: var(--u2-b2-bg); } html.b #u2conf a.b:hover { background: var(--btn-h-bg); } #u2conf .c label { font-size: 1.6em; width: 2em; height: 1em; padding: .4em 0; display: block; border-radius: .25em; } #u2conf input[type="checkbox"] { position: relative; opacity: .02; top: 2em; } #u2conf input[type="checkbox"]+label, #u2conf input[type="checkbox"]:checked+label { position: relative; cursor: pointer; background: var(--btn-bg); box-shadow: var(--btn-bs); border-bottom: var(--btn-bb); text-shadow: 1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000; } #u2conf input[type="checkbox"]:checked+label { background: var(--btn-1-bg); box-shadow: var(--btn-1-bs); border-bottom: var(--btn-1-bb); } #u2conf input[type="checkbox"]+label:hover { background: var(--btn-h-bg); box-shadow: var(--btn-h-bs); border-bottom: var(--btn-h-bb); } #u2conf input[type="checkbox"]:checked+label:hover { background: var(--btn-1h-bg); box-shadow: var(--btn-1h-bs); border-bottom: var(--btn-1h-bb); } #op_up2k.srch #u2conf td:nth-child(2)>*, #op_up2k.srch #u2conf td:nth-child(3)>* { background: #777; border-color: var(--fg); box-shadow: none; opacity: .2; } #u2flagblock { display: none; margin: 2em auto 4em auto; padding: .5em; max-width: 27em; text-align: center; border: .2em dashed rgba(128, 128, 128, 0.3); } #u2foot, #u2life { color: var(--fg-max); text-align: center; margin: .8em 0; } #u2life { margin: 2.5em 0; line-height: 1.5em; } #u2life div { display: inline-block; white-space: nowrap; margin: 0 2em; } #u2life div:first-child { margin-bottom: .2em; } #u2life small { opacity: .6; } #lifew { border-bottom: 1px dotted var(--fg-max); } #u2foot { font-size: 1.2em; font-style: italic; } #u2foot .warn { font-size: 1.2em; padding: .5em .8em; margin: 1em -.6em; border-width: .1em 0; text-align: center; } #u2foot .warn span { color: var(--srv-3); } #u2foot span { color: #999; font-size: .9em; font-weight: normal; } #u2foot>*+* { margin-top: 1.5em; } #u2life input { width: 4em; text-align: right; } .prog { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; } #u2tab span.inf, #u2tab span.ok, #u2tab span.err { color: #fff; padding: .22em; border-radius: .2em; border: 2px solid #f0f; } #u2tab span.inf { background: var(--u2-inf-bg); border-color: var(--u2-inf-b1); } #u2tab span.ok { background: var(--u2-ok-bg); border-color: var(--u2-ok-b1); } #u2tab span.err { background: var(--u2-err-bg); border-color: var(--u2-err-b1); } #u2tab a>span, #docul .bn a>span, #unpost a>span { font-weight: bold; font-style: italic; color: var(--fg-max); padding-left: .2em; } .fsearch_explain { color: var(--a-dark); padding-left: .7em; font-size: 1.1em; line-height: 0; } html.c #path, html.a #path { border-radius: 0 .3em .3em 0; } html.c #pctl a, html.a #pctl a { background: rgba(0,0,0,0.1); margin-right: .5em; box-shadow: -.02em -.02em .3em rgba(0,0,0,0.2) inset; } html.d #pctl, html.b #pctl { left: .5em; } html.d #ops, html.c #ops, html.a #ops { margin: 1.7em 1.5em 0 1.5em; border-radius: .3em; border-width: 1px 0; } html.c .opbox, html.a .opbox { margin: 1.5em 0 0 0; } html.dz .opview input.i { width: calc(100% - 18em); } html.c #tree, html.c #treeh, html.a #tree, html.a #treeh { border-radius: 0 .3em 0 0; background: var(--bg-u2); } html.c #treepar, html.a #treepar { background: var(--bg-u2); } html.c #tree li, html.a #tree li { border-top: 1px solid var(--bg-u5); border-bottom: 1px solid var(--bg-d3); } html.c #tree li:last-child, html.a #tree li:last-child { border-bottom: none; } html.c .opbox h3, html.a .opbox h3 { border-bottom: 1px solid var(--bg-u5); } html.c #ops, html.c .opbox, html.c #path, html.c #srch_form, html.c .ghead, html.a #ops, html.a .opbox, html.a #path, html.a #srch_form, html.a .ghead { background: var(--bg-u2); border: 1px solid var(--bg-u3); box-shadow: 0 0 .3em var(--bg-d3); } html.c #u2btn, html.a #u2btn { color: #eee; background: var(--bg-u5); background: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%); background: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%); background: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%); box-shadow: .4em .4em 0 var(--bg-d3); border: 1px solid #222; } html.ay #u2btn { box-shadow: .4em .4em 0 #ccc; } html.dz #u2btn { letter-spacing: -.033em; } html.c #u2conf.ww #u2btn, html.a #u2conf.ww #u2btn { margin: -2em .5em -3em 0; padding: .9em 0; } html.c #op_up2k.srch #u2btn, html.a #op_up2k.srch #u2btn { background: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%); text-shadow: 1px 1px 1px #fc6; color: #333; } html.c #u2conf #u2btn, html.a #u2conf #u2btn { padding: .6em 0; margin-top: -2.6em; } html.c #u2etas, html.a #u2etas { background: var(--bg-d1); border: 1px solid var(--bg-u1); border-width: .1em 0; border-radius: .5em; border-width: .25em 0; } html.c #u2cards, html.a #u2cards { margin: 0 auto -1em auto; } html.c #u2foot:empty, html.a #u2foot:empty { margin-bottom: -1em; } html.ay #ops, html.ay .opbox, html.ay #path, html.ay #doc, html.ay #srch_form, html.ay .ghead, html.ay #u2etas { border-color: var(--bg-u2); box-shadow: 0 0 .3em var(--bg-u5); } html.ay #tree li, html.ay #treepar { border-color: #f7f7f7 var(--bg-max) #ddd var(--bg-max); } html.ay #path { background: #f7f7f7; box-shadow: 0 0 .3em #bbb; } html.ay #treeh, html.ay #treepar { background: #f7f7f7; border-color: #ddd; } html.ay #tree { border-color: #ddd; box-shadow: 0 0 1em #ddd; background: #f7f7f7; } html.b #path { margin: .3em 0; line-height: 1.7em; } html.b.op_open #path { margin-top: .2em; } html.b #srv_info { display: none; } html.b #srv_info2 { display: inline-block; } html.b #srv_info2:after { content: '//'; margin: 0 .4em; } html.b #acc_info { right: .5em; } html.b #wtoggle { border-radius: .1em 0 0 0; } html.d #barpos, html.d #barbuf, html.d #pvol, html.b #barpos, html.b #barbuf, html.b #pvol { border-radius: .2em; } html.b #barpos { box-shadow: 0 0 0 1px rgba(0,0,0,0.4); } html.by #barpos { box-shadow: 0 0 0 1px rgba(0,0,0,0.2) inset; } html.b #ops { max-width: 1em; margin-bottom: 1.7em; position: relative; z-index: 2; } html.b #ops a { background: var(--bg); } html.b .opview { margin: 1em 0; } html.b #srch_q { margin: .2em 0 0 1.6em; } html.b #srch_q:empty { margin-bottom: -1em; } html.b #op_up2k { margin-top: 3em; } html.b #tree { box-shadow: 0 -1px 0 rgba(128,128,128,0.4); } html.bz #tree { box-shadow: 0 -1px 0 var(--bg-d3); } html.b #treeh, html.b #tree li { border: none; } html.b #tree li { margin-left: .8em; } html.b #docul a, html.b .ntree a { padding: .6em .2em; } html.b #treepar { margin-left: .63em; width: calc(.1em + var(--nav-sz) - var(--sbw)); border-bottom: .2em solid var(--f-h-b1); } html.b #wrap { margin-top: 2em; } html.by .ghead, html.bz .ghead { background: var(--bg); padding: .2em 0; } html.b #files td { padding: .5em .7em; } html.b .btn { top: -.1em; } html.b #op_up2k.srch sup { color: #fc0; } html.by #u2btn sup { color: #06b; } html.by #op_up2k.srch sup { color: #b70; } html.bz #u2cards a.act { box-shadow: 0 -.1em .2em var(--bg-d2); } html.b #u2conf { margin: 2em auto 0 auto; } html.b #u2conf .txtbox { border: none; } html.b #u2conf a.b { border-radius: .2em; } html.by #u2cards a.act { border-width: 2px; } html.cy .mdo a { background: #f00; } html.cy #wrap, html.cy #acc_info a, html.cy #op_up2k, html.cy #files, html.cy #files a, html.cy #files tbody div a:last-child { color: #000; } html.cy #u2tab a, html.cy #u2cards a { color: #f00; } html.cy #unpost a { color: #ff0; } html.cy #barbuf { filter: hue-rotate(267deg) brightness(0.8) contrast(4); } html.cy #pvol { filter: hue-rotate(4deg) contrast(2.2); } html.dz * { border-radius: 0 !important; } html.d #treepar { border-bottom: .2em solid var(--f-h-b1); } @media (max-width: 32em) { #u2conf { font-size: .9em; } } @media (max-width: 28em) { #u2conf { font-size: .8em; } } @media (min-width: 70em) { #barpos, #barbuf { width: calc(100% - 21em); left: 9.8em; top: .7em; height: 1.6em; bottom: auto; } html.d #barpos, html.b #barpos, html.d #barbuf, html.b #barbuf { width: calc(100% - 19em); left: 8em; } #widget { bottom: -3.2em; height: 3.2em; } #pvol { max-width: 9em; } html.d #ops, html.b #ops { padding-left: 1.7em; } html.d .opview, html.b .opview { margin: 1em; } html.d #path, html.b #path { padding-left: 1.3em; } } @media (max-width: 35em) { #ops>a[data-dest="new_md"], #ops>a[data-dest="msg"] { display: none; } #op_mkdir.act+div, #op_mkdir.act+div+div { display: block; margin-top: 1em; } } @media (max-width: 54em) { html.b #ops { margin-top: 1.7em; } } @supports (display: grid) and (gap: 1em) { #ggrid { display: grid; margin: 0em 0.25em; padding: unset; grid-template-columns: repeat(auto-fit,var(--grid-sz)); justify-content: center; gap: 1em; } html.b #ggrid { padding: 0 0 2em 0; gap: 1em 1.5em; } #ggrid > a { margin: unset; padding: unset; } #ggrid>a>span { text-align: center; padding: .2em .2em .15em .2em; } } @media (prefers-reduced-motion) { @keyframes spin { } @keyframes gexit { } @keyframes bounce { } @keyframes bounceFromLeft { } @keyframes bounceFromRight { } #ggrid>a:before, #widget.anim, #u2tabw, .dropdesc, .dropdesc b, .dropdesc>div>div { transition: none; } #bbox-next, #bbox-prev, #bbox-btns { transition: background-color .3s ease, color .3s ease; } } html.ey { --negative-space: 0em; /* Use this to change the global spacing of the 95 theme */ --font-main: consolas; --font-serif: consolas; --font-mono: consolas; --w: #fff; --w2: #dfdfdf; --w3: grey; --fg: #000; --fg-max: #0000ff; --fg-weak: #0000ff; --bg: #c6c3c6; --bg-d3: #ff0; --bg-d2: var(--w3); --bg-d1: var(--bg); --bg-u2: var(--bg); --bg-u3: var(--bg); --bg-u5: var(--shadow-color-2); --tab-alt: #00f; --g-fsel-bg: #00f; --g-sel-bg: #00f; --g-fsel-b1: #fff; --row-alt: var(--w); --scroll: var(--silver); --f-sel-sh: transparent; --a: #000; --a-b: #fff; --a-hil: #fff; --a-h-bg: var(--bg); --a-dark: var(--a); --a-gray: var(--fg-weak); --btn-fg: var(--fg); --btn-bg: var(--bg); --btn-h-fg: var(--fg); --btn-h-bg: var(--bg); --btn-1-fg: var(--fg); --btn-1-bg: var(--bg); --btn-1h-bg: var(--bg-d3); --txt-sh: a; --txt-bg: var(--white); --u2-b1-bg: var(--w2); --u2-b2-bg: var(--w2); --u2-txt-bg: var(--w2); --u2-tab-bg: a; --u2-tab-1-bg: var(--w2); --sort-1: var(--fg-weak); --tree-bg: var(--w); --g-b1: a; --g-b2: a; --g-f-bg: var(--w2); --f-sh1: 0.1; --f-sh2: 0.02; --f-sh3: 0.1; --f-h-b1: a; --srv-1: var(--w); --srv-3: var(--a); --mp-sh: a; --black: #000; --white: #fff; --grey: grey; --silver: silver; --transparent: transparent; --shadow-color-1: #0a0a0a; --shadow-color-2: #808080; --border-dashed-black: 1px dashed var(--black); --radius: 0; --focus-outline: 1px dashed var(--black); --hover-outline: 1px dotted var(--black); --fm-off: var(--w3); --ttlbar: linear-gradient(90deg, navy, #1084d0); --inset-bg: var(--white); --scroll-bkg: var(--white); /*All sides*/ --shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px var(--white), inset -2px -2px var(--grey), inset 2px 2px var(--w2); --shadow-inset: inset -1px -1px var(--white), inset 1px 1px var(--shadow-color-1), inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-2); --shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--grey), inset -2px -2px var(--w2), inset 2px 2px var(--shadow-color-1); /*Indiv sides*/ --shadow-outset-bottom: inset 0 -1px var(--shadow-color-1), inset 0 -2px var(--grey); --shadow-outset-right: inset -1px 0 var(--shadow-color-1), inset -2px 0 var(--grey); --shadow-outset-left: inset 1px 0 var(--white), inset 2px 0 var(--w2); --shadow-outset-top: inset 0 1px var(--white), inset 0 2px var(--w2); --shadow-inset-bottom: inset 0 -1px var(--white), inset 0 -2px var(--w2); --shadow-inset-right: inset -1px 0 var(--white), inset -2px 0 var(--w2); --shadow-inset-left: inset 1px 0 var(--shadow-color-1), inset 2px 0 var(--shadow-color-2); --shadow-inset-top: inset 0 1px var(--shadow-color-1), inset 0 2px var(--shadow-color-2); } html.ez { --negative-space: 0em; /* Use this to change the global spacing of your theme :) */ --font-main: consolas; --font-serif: consolas; --font-mono: consolas; --w: #fff; --w2: var(--inset-bg); --w3: grey; --fg: #cfcfcf; --fg-max: #47b8ff; --fg-weak: #47b8ff; --bg: #383838; --bg-d3: #600000; --bg-d2: var(--shadow-color-1); --bg-d1: var(--bg); --u2-tab-1-fg: #ff0; --bg-u2: var(--bg); --bg-u3: var(--bg); --bg-u5: var(--shadow-color-2); --tab-alt: #47b8ff; --g-fsel-bg: #0000b7; --g-sel-bg: #00f; --g-fsel-b1: #fff; --row-alt: #555555; --scroll: #555555; --f-sel-sh: transparent; --a: var(--fg); --a-b: var(--fg); --a-hil: var(--fg); --btn-1h-bg: var(--bg-d3); --a-h-bg: var(--bg); --a-dark: var(--a); --a-gray: var(--fg-weak); --btn-fg: var(--white); --btn-bg: var(--bg); --btn-h-fg: var(--white); --btn-h-bg: var(--bg); --btn-1-fg: var(--white); --btn-1-bg: var(--bg); --txt-sh: a; --u2-b1-bg: var(--w2); --u2-b2-bg: var(--w2); --u2-txt-bg: var(--w2); --u2-tab-bg: a; --u2-tab-1-bg: var(--w2); --sort-1: var(--fg-weak); --g-b1: a; --g-b2: a; --g-f-bg: var(--w2); --f-sh1: 0.1; --f-sh2: 0.02; --f-sh3: 0.1; --f-h-b1: a; --srv-1: var(--w); --srv-3: var(--a); --mp-sh: a; --black: #000; --white: #fff; --grey: grey; --silver: #858585; --transparent: transparent; --shadow-color-1: #101010; --shadow-color-2: #1f1f1f; --border-dashed-black: 1px dashed var(--shadow-color-1); --radius: 0; --focus-outline: 1px dashed var(--white); --hover-outline: 1px dotted var(--white); --fm-off: var(--w3); --ttlbar: linear-gradient(90deg, var(--shadow-color-1) 20%, #888888); --inset-bg: #3f3f3f; --tree-bg: var(--inset-bg); --txt-bg: var(--inset-bg); --scroll-bkg: var(--black); /*All sides*/ --shadow-outset: inset -1px -1px var(--shadow-color-1), inset 1px 1px #878787, inset -2px -2px var(--shadow-color-2), inset 2px 2px #575757; --shadow-inset: inset -1px -1px #878787, inset 1px 1px var(--shadow-color-1), inset -2px -2px #575757, inset 2px 2px var(--shadow-color-2); --shadow-input: inset -1px -1px var(--white), inset 1px 1px var(--shadow-color-2), inset -2px -2px #575757, inset 2px 2px var(--shadow-color-1); --shadow-outset-bottom: inset 0 -1px var(--shadow-color-1), inset 0 -2px var(--shadow-color-2); --shadow-outset-right: inset -1px 0 var(--shadow-color-1), inset -2px 0 var(--shadow-color-2); --shadow-outset-left: inset 1px 0 #878787, inset 2px 0 #575757; --shadow-outset-top: inset 0 1px #878787, inset 0 2px #575757; --shadow-inset-bottom: inset 0 -1px #878787, inset 0 -2px #575757; --shadow-inset-right: inset -1px 0 #878787, inset -2px 0 #575757; --shadow-inset-left: inset 1px 0 var(--shadow-color-1), inset 2px 0 var(--shadow-color-2); --shadow-inset-top: inset 0 1px var(--shadow-color-1), inset 0 2px var(--shadow-color-2); } html.e { text-shadow: none; } html.e #files, html.e #u2conf input[type="checkbox"]:hover + label, html.e .tgl.btn.on:hover, html.e body { background: var(--bg); } html.e #pctl a, html.e #repl, html.e #u2conf a, html.e #u2conf input[type="checkbox"] + label, html.e #wfp a, html.e .btn, html.e .eq_step, html.e input[type="submit"] { box-shadow: var(--shadow-outset); border-radius: var(--radius); background: var(--bg); border: 0; } a.s0r, html.e #ghead a.s0, html.e #u2conf input[type="checkbox"]:checked + label, html.e .tgl.btn.on, html.e input[type="submit"]:active { box-shadow: var(--shadow-inset) !important; } html.e #ops a:hover, html.e #pctl a:hover, html.e #repl:hover, html.e #u2conf a:hover, html.e #u2conf input[type="checkbox"]:hover + label, html.e #wfp a:hover, html.e .btn:hover, html.e .eq_step:hover, html.e input[type="submit"]:hover { outline: var(--hover-outline); outline-offset: -4px; } html.e .ntree a:hover, html.e :focus, html.e :focus + label, html.e a:active, html.e tr:focus, input[type="text"]:focus { outline: var(--focus-outline) !important; } html.e tr:focus { box-shadow: none; } html.e #pctl a:focus, html.e #repl:hover, html.e #u2conf input[type="checkbox"]:focus + label, html.e #wfp a:focus, html.e .btn:focus, html.e .eq_step:focus { border: 0 !important; outline: var(--focus-outline) !important; outline-offset: 2px; box-shadow: var(--shadow-outset) !important; } html.e #files tbody, html.e #u2cards a.act { box-shadow: var(--shadow-inset); } html.e #files { border: 2px groove var(--transparent); box-sizing: border-box; width: 100%; padding: 0.3em; top: 0; border: 0; } html.e #files tbody tr td, html.e #files thead th { border-radius: var(--radius); } #files td { background: var(--w2); } html.e #files tr { background-color: var(--black); } html.e #srv_info span, html.e label { color: var(--btn-fg) !important; } html.e #acc_info { background: var(--transparent); color: var(--white); height: 2em; left: 1em; width: fit-content; } html.e #acc_info, html.e #ops, html.e #srv_info { display: flex; align-items: center; } html.e #acc_info span.warn, html.e #acc_info a { color: var(--white); } html.e #flogout:before { padding-left: 0.2em; padding-right: 0.4em; content: " | "; } html.e #blogout { color: var(--w); box-shadow: none; background: transparent; } html.e .opwide > div { border-left: 1px solid var(--fg); } html.e #srv_info { background: var(--transparent); color: var(--white); height: fit-content; top: 3.2em; left: 1em; gap: 0.2em; } html.e #u2cards a.act { padding: 0.2em 1em; } html.e #u2btn { border: var(--border-dashed-black); border-radius: var(--border-radius); transform: translateY(30%); } html.e #ops, html.e #ops a { border-radius: var(--radius); } @media only screen and (max-width: 600px) { html.e #acc_info { background: var(--transparent); color: var(--white); height: fit-content; align-items: center; top: 3.2em; right: 1em; left: auto; display: flex; gap: 0.2em; } html.e #u2btn { transform: none; } } html.e #ops { background: var(--ttlbar); /*HC*/ box-shadow: inset 0-1px grey, inset 0-2px var(--shadow-color-1); height: 2em; gap: 0.6em; padding: 0.2em; flex-direction: row-reverse; margin-bottom: 1.2em; } html.e #srch_form, html.e .opbox { padding-bottom: 1em; padding-top: 1em; max-width: 100vw; } html.e #ghead, html.e #ops a { align-items: center; display: flex; } html.e #ops a { text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); height: 1.4em; padding: 0; box-shadow: var(--shadow-outset); background: var(--bg); aspect-ratio: 1/1; justify-content: center; font-size: 1.25em; z-index: 4; } html.e #blogout:focus, html.e #ops a:focus { outline: 1px dashed var(--w) !important; } html.e #blogout:hover { text-decoration: underline; } html.e #ops > a:not(:first-child).act { height: 1.4em; width: 1.4em; padding-bottom: 0.3em; margin-top: 0.3em; border-top-left-radius: 3px; border-top-right-radius: 3px; box-shadow: var(--shadow-inset-left), var(--shadow-inset-top), var(--shadow-inset-right); z-index: 6; } html.e #ops a.act { box-shadow: var(--shadow-inset); border-bottom: 0; } html.e a:active { border: 0; } html.e :focus, html.e :focus + label { border: 0 !important; outline-offset: 1px; border-radius: var(--radius) !important; box-shadow: inherit; } html.e #opa_x { text-shadow: 0 0 0 var(--transparent) !important; color: var(--bg) !important; display: flex; } html.e #opa_x:before { content: "⨯"; color: var(--fg) !important; margin-top: -0.1em; font-size: 1.75em; position: absolute; } html.e .opbox { margin: -1.2em 0 0; box-shadow: var(--shadow-inset-bottom), var(--shadow-inset-left), var(--shadow-inset-right); border-radius: var(--radius); z-index: 5; background: var(--bg); } html.e #srch_form { margin: 0; border-radius: var(--radius); } html.e #op_unpost { max-width: 100vw; margin: 0; } html.e label:focus { box-shadow: 0 0; } html.e #tree { box-shadow: none; padding-right: 5px; } html.e #tt { background: var(--w2); } html.e .mdo a { background: 0 0; text-decoration: underline; } html.e .mdo code, html.e .mdo pre { color: var(--white); background: var(--w2); border: 0; } html.e .mdo h1, html.e .mdo h2 { background: 0 0; border-color: var(--w2); } html.e #tt, html.e .mdo ol ol, html.e .mdo ol ul, html.e .mdo ul ol, html.e .mdo ul ul { border-color: var(--w2); } html.e .mdo li > em, html.e .mdo p > em, html.e .mdo td > em { color: #fd0; } html.e input.txtbox, html.e input[type="text"], html.e select { background-color: var(--txt-bg); box-shadow: var(--shadow-input) !important; box-sizing: border-box; padding: 3px 4px; border-radius: var(--radius); border: 0; } html.e #gfiles { box-shadow: var(--shadow-outset); background: var(--bg); padding: 0.4em; display: flex; flex-direction: column; gap: 0.3em; } html.e #ggrid { background-color: var(--inset-bg); box-shadow: var(--shadow-input); padding: 1.5em; margin: 0; overflow-x: scroll; } html.e #ghead { margin: 0; justify-content: flex-end; gap: 0.4em; padding: 0; overflow: auto; top: 0px; border-radius: 0px; } html.e #ghead a { margin: 0; border-radius: var(--radius); } html.e ::-webkit-scrollbar, html.e::-webkit-scrollbar { width: 16px !important; height: 16px !important; background: var(--transparent) !important; } html.e ::-webkit-scrollbar-button, html.e ::-webkit-scrollbar-thumb, html.e::-webkit-scrollbar-button, html.e::-webkit-scrollbar-thumb { width: 16px !important; height: 16px !important; background: var(--scroll) !important; /*HC*/ box-shadow: var(--shadow-outset); border: 1px solid !important; border-color: var(--silver) var(--black) var(--black) var(--silver) !important; } html.e ::-webkit-scrollbar-track, html.e::-webkit-scrollbar-track { image-rendering: optimize-contrast !important; background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAyIDIiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CjxtZXRhZGF0YT5NYWRlIHdpdGggUGl4ZWxzIHRvIFN2ZyBodHRwczovL2NvZGVwZW4uaW8vc2hzaGF3L3Blbi9YYnh2Tmo8L21ldGFkYXRhPgo8cGF0aCBzdHJva2U9IiNjMGMwYzAiIGQ9Ik0wIDBoMU0xIDFoMSIgLz4KPC9zdmc+) !important; background-position: 0 0 !important; background-repeat: repeat !important; background-size: 2px !important; background: var(--scroll-bkg); } #tree::-webkit-scrollbar, #tree::-webkit-scrollbar-track { background: var(--scroll-bkg); } html.e ::-webkit-scrollbar-button, html.e::-webkit-scrollbar-button { background-repeat: no-repeat !important; background-size: 16px !important; } html.e ::-webkit-scrollbar-button:single-button:vertical:decrement, html.e::-webkit-scrollbar-button:single-button:vertical:decrement { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTcgNWgxTTYgNmgzTTUgN2g1TTQgOGg3IiAvPgo8L3N2Zz4=) !important; } html.e ::-webkit-scrollbar-button:single-button:vertical:increment, html.e::-webkit-scrollbar-button:single-button:vertical:increment { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTQgNWg3TTUgNmg1TTYgN2gzTTcgOGgxIiAvPgo8L3N2Zz4=) !important; } html.e ::-webkit-scrollbar-button:single-button:horizontal:decrement, html.e::-webkit-scrollbar-button:single-button:horizontal:decrement { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTggM2gxTTcgNGgyTTYgNWgzTTUgNmg0TTYgN2gzTTcgOGgyTTggOWgxIiAvPgo8L3N2Zz4=) !important; } html.e ::-webkit-scrollbar-button:single-button:horizontal:increment, html.e::-webkit-scrollbar-button:single-button:horizontal:increment { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTAuNSAxNiAxNiIgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KPG1ldGFkYXRhPk1hZGUgd2l0aCBQaXhlbHMgdG8gU3ZnIGh0dHBzOi8vY29kZXBlbi5pby9zaHNoYXcvcGVuL1hieHZOajwvbWV0YWRhdGE+CjxwYXRoIHN0cm9rZT0iIzAwMDAwMCIgZD0iTTYgM2gxTTYgNGgyTTYgNWgzTTYgNmg0TTYgN2gzTTYgOGgyTTYgOWgxIiAvPgo8L3N2Zz4=) !important; } html.e ::-webkit-scrollbar-corner, html.e::-webkit-scrollbar-corner { background: var(--silver) !important; } html, html.e #tree { scrollbar-color: inherit !important; } html.e #tree { background: var(--bg); padding-left: 0.4em; padding-top: 0; margin-left: var(--negative-space); } html.e.noscroll #tree { /*HC*/ box-shadow: 1px 1px var(--grey), 2px 2px var(--shadow-color-1), var(--shadow-outset-bottom); } html.e #treeh { background: var(--bg); box-shadow: var(--shadow-outset-top), var(--shadow-outset-bottom); width: calc(1.5em + var(--nav-sz) - var(--sbw)); height: 2.4em; border: none; top: -2px; display: flex; align-items: center; gap: 0.6em; } html.e #treeh .btn { margin: 0px; top: auto; } html.e #tree ul { border-left: var(--border-dashed-black); margin-left: 2.15em; } html.e .ntree a:first-child { font-family: scp, monospace, monospace; font-size: 1.2em; line-height: 0; background: var(--inset-bg); aspect-ratio: 1/1; text-align: center; align-content: center; border-radius: var(--radius) !important; padding: 0.057em; border: 1px solid var(--black); } html.e .ntree a:first-child:after { content: "."; position: absolute; border-top: var(--border-dashed-black); color: var(--transparent); font-size: 0.9em; margin-left: 0.13em; } html.e #treeul { border: 0 !important; position: static; margin: 0 !important; min-height: 100%; height: max-content; } html.e .ntree a:last-of-type:before { content: "📁"; margin-left: 0.3em; } html.e .ntree { padding-left: 1em !important; padding-top: 0.3em !important; background: var(--inset-bg); box-shadow: var(--shadow-inset-left), var(--shadow-inset-bottom); } html.e #tree li { margin-left: -0.5em; border-top: 0; } html.e .ntree a:hover { outline-offset: -2px; color: var(--fg); border-radius: var(--radius) !important; } html.e #treepar { width: calc(-1em + var(--nav-sz) - var(--sbw)); overflow: hidden; left: -0.7em; box-shadow: var(--shadow-inset-left), var(--shadow-inset-top); border-left: 0 !important; border-bottom: var(--border-dashed-black); margin-left: calc(2.1em - (1em - var(--negative-space))) !important; } html.e #path, html.e #widgeti, html.e #wtoggle, html.e #wtoggle a, html.e #files, html.e #files thead th, html.e #ghead a, html.e #tree { box-shadow: var(--shadow-outset); } html.e.noscroll #treepar { width: calc(var(--nav-sz) - 1em); } html.e #docul { border-left: 0 !important; margin-left: 0 !important; } html.e #wrap { transform: translateX(calc((var(--negative-space) * 2) - 1.2em)); padding-right: var(--negative-space); position: relative; margin-right: calc((var(--negative-space) * 2) - 1.2em); margin-top: var(--negative-space); margin-left: 1.2em; /*overflow-x: auto; fix for OOB table when screen space is limited (mobile), but removes sticky header*/ } html.e input[type="radio"] { accent-color: #232323; } html.e #path { width: calc(100% - 0.4em); display: flex; align-items: center; margin: 0; padding: 0.2em; overflow-x: auto; } html.e #path i { border: 1px solid var(--w); border-color: var(--w); margin: 0; border-width: 0.1em 0.1em 0 0; height: 0.5em; width: 0.5em; }/* html.e #hovertree:after { color: red; content: "BUGGY"; html.ez #hovertree:after { color: rgb(255 98 98); content: "BUGGY"; } }*/ html.e #widget { box-shadow: 0 0; border: 0 !important; } html.e #wtico, html.e #zip1 { box-shadow: 0 0 !important; } html.e #wtgrid { top: -0.09em; } html.e #wfs, html.e #wm3u, html.e #wnp, html.e #wzip { border-width: 0 1px 0 0; } html.e #wfm.act + #wzip1 + #wzip, html.e #wfm.act + #wzip1 + #wzip + #wnp { border-left-width: 1px; } html.e #barpos { /* border-radius: var(--radius); */ box-shadow: var(--shadow-inset); } html.e #goh + span { border-left: 0.1em solid var(--bg-u5); } html.e #wfp { margin: var(--negative-space); font-size: 0; display: inline-block; } html.e #wfp a { font-size: large; display: inline-block; } html.e #repl { font-size: large; padding: 0.33em; right: calc(var(--negative-space) * 0.89); position: absolute; } html.e #epi { text-align: center; text-wrap-mode: nowrap; margin: 0px; } html.e #epi.logue:not(.mdo) { padding: 0.8em; box-shadow: var(--shadow-outset); } html.e #epi.logue.mdo { padding-left: 3px; } html.e #doc { box-shadow: var(--shadow-inset); background: var(--inset-bg); margin: 0.2em; border-radius: var(--radius); } html.e #detree { padding: 0px; } #rcm { position: absolute; display: none; background: #fff; background: var(--bg); border: 1px solid var(--bg-u3); outline: none; border-radius: .3rem; box-shadow: 0 0 .3rem var(--bg-d3); z-index: 3; } #rcm > * { display: block; } #rcm > a { padding: .2rem .3rem; } #rcm > .hide { display: none; } #rcm > a:hover { background: var(--bg-d2); } #rcm > .sep { margin: 0 .2rem; border-bottom: 1px solid var(--a-gray); } #tempname { color: var(--fg); background: var(--txt-bg); border: none; box-shadow: 0 0 2px var(--txt-sh); border-bottom: 1px solid #999; border-color: var(--a); border-radius: .2em; padding: .2em .3em; } .selbox { position: fixed; border: .5em solid #f0f; border: .2em solid var(--btn-1h-bg); background-color: rgba(128, 128, 128, 0.6); background-color: rgb(from var(--btn-1h-bg) r g b / 0.5); pointer-events: none; z-index: 99; } ================================================ FILE: copyparty/web/browser.html ================================================ {{ title }} {{ html_head }} {%- if css %} {%- endif %}
    📂
    📝
    📟

    🌲 {%- for n in vpnodes %} {{ n[1] }} {%- endfor %}

    {%- if doc %}
    {{ doc|e }}
    {%- else %}
    {%- endif %}
    {{ "" if sb_lg else logues[0] }}
    {%- for k in taglist %} {%- if k.startswith('.') %} {%- else %} {%- endif %} {%- endfor %} {%- for f in files %} {%- if f.tags is defined %} {%- for k in taglist %}{%- endfor %} {%- endif %} {%- endfor %}
    c File Name Size{{ k[1:] }}{{ k[0]|upper }}{{ k[1:] }}T Date
    {{ f.lead }}{{ f.name|e }}{{ f.sz }}{{ f.tags[k]|e }}{{ f.ext }}{{ f.dt }}
    {{ "" if sb_lg else logues[1] }}

    control-panel

    π
    {{ srv_info }}
    {%- if lang != "eng" %} {%- endif %} {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/browser.js ================================================ "use strict"; var J_BRW = 1; if (window.rw_edit === undefined) alert('FATAL ERROR: receiving stale data from the server; this may be due to a broken reverse-proxy (stuck cache). Try restarting copyparty and press CTRL-SHIFT-R in the browser'); var XHR = XMLHttpRequest; if (1) Ls.eng = { "tt": "English", "cols": { "c": "action buttons", "dur": "duration", "q": "quality / bitrate", "Ac": "audio codec", "Vc": "video codec", "Fmt": "format / container", "Ahash": "audio checksum", "Vhash": "video checksum", "Res": "resolution", "T": "filetype", "aq": "audio quality / bitrate", "vq": "video quality / bitrate", "pixfmt": "subsampling / pixel structure", "resw": "horizontal resolution", "resh": "vertical resolution", "chs": "audio channels", "hz": "sample rate", }, "hks": [ [ "misc", ["ESC", "close various things"], "file-manager", ["G", "toggle list / grid view"], ["T", "toggle thumbnails / icons"], ["⇧ A/D", "thumbnail size"], ["ctrl-K", "delete selected"], ["ctrl-X", "cut selection to clipboard"], ["ctrl-C", "copy selection to clipboard"], ["ctrl-V", "paste (move/copy) here"], ["Y", "download selected"], ["F2", "rename selected"], "file-list-sel", ["space", "toggle file selection"], ["↑/↓", "move selection cursor"], ["ctrl ↑/↓", "move cursor and viewport"], ["⇧ ↑/↓", "select prev/next file"], ["ctrl-A", "select all files / folders"], ], [ "navigation", ["B", "toggle breadcrumbs / navpane"], ["I/K", "prev/next folder"], ["M", "parent folder (or unexpand current)"], ["V", "toggle folders / textfiles in navpane"], ["A/D", "navpane size"], ], [ "audio-player", ["J/L", "prev/next song"], ["U/O", "skip 10sec back/fwd"], ["0..9", "jump to 0%..90%"], ["P", "play/pause (also initiates)"], ["S", "select playing song"], ["Y", "download song"], ], [ "image-viewer", ["J/L, ←/→", "prev/next pic"], ["Home/End", "first/last pic"], ["F", "fullscreen"], ["R", "rotate clockwise"], ["⇧ R", "rotate ccw"], ["S", "select pic"], ["Y", "download pic"], ], [ "video-player", ["U/O", "skip 10sec back/fwd"], ["P/K/Space", "play/pause"], ["C", "continue playing next"], ["V", "loop"], ["M", "mute"], ["[ and ]", "set loop interval"], ], [ "textfile-viewer", ["I/K", "prev/next file"], ["M", "close textfile"], ["E", "edit textfile"], ["S", "select file (for cut/copy/rename)"], ["Y", "download textfile"], ["⇧ J", "beautify json"], ] ], "m_ok": "OK", "m_ng": "Cancel", "enable": "Enable", "danger": "DANGER", "clipped": "copied to clipboard", "ht_s1": "second", "ht_s2": "seconds", "ht_m1": "minute", "ht_m2": "minutes", "ht_h1": "hour", "ht_h2": "hours", "ht_d1": "day", "ht_d2": "days", "ht_and": " and ", "goh": "control-panel", "gop": 'previous sibling">prev', "gou": 'parent folder">up', "gon": 'next folder">next', "logout": "Logout ", "login": "Login", "access": " access", "ot_close": "close submenu", "ot_search": "`search for files by attributes, path / name, music tags, or any combination of those$N$N`foo bar` = must contain both «foo» and «bar»,$N`foo -bar` = must contain «foo» but not «bar»,$N`^yana .opus$` = start with «yana» and be an «opus» file$N`"try unite"` = contain exactly «try unite»$N$Nthe date format is iso-8601, like$N`2009-12-31` or `2020-09-12 23:30:00`", "ot_unpost": "unpost: delete your recent uploads, or abort unfinished ones", "ot_bup": "bup: basic uploader, even supports netscape 4.0", "ot_mkdir": "mkdir: create a new directory", "ot_md": "new-file: create a new textfile", "ot_msg": "msg: send a message to the server log", "ot_mp": "media player options", "ot_cfg": "configuration options", "ot_u2i": 'up2k: upload files (if you have write-access) or toggle into the search-mode to see if they exist somewhere on the server$N$Nuploads are resumable, multithreaded, and file timestamps are preserved, but it uses more CPU than [🎈]  (the basic uploader)

    during uploads, this icon becomes a progress indicator!', "ot_u2w": 'up2k: upload files with resume support (close your browser and drop the same files in later)$N$Nmultithreaded, and file timestamps are preserved, but it uses more CPU than [🎈]  (the basic uploader)

    during uploads, this icon becomes a progress indicator!', "ot_noie": 'Please use Chrome / Firefox / Edge', "ab_mkdir": "make directory", "ab_mkdoc": "new textfile", "ab_msg": "send msg to srv log", "ay_path": "skip to folders", "ay_files": "skip to files", "wt_ren": "rename selected items$NHotkey: F2", "wt_del": "delete selected items$NHotkey: ctrl-K", "wt_cut": "cut selected items <small>(then paste somewhere else)</small>$NHotkey: ctrl-X", "wt_cpy": "copy selected items to clipboard$N(to paste them somewhere else)$NHotkey: ctrl-C", "wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V", "wt_selall": "select all files$NHotkey: ctrl-A (when file focused)", "wt_selinv": "invert selection", "wt_zip1": "download this folder as archive", "wt_selzip": "download selection as archive", "wt_seldl": "download selection as separate files$NHotkey: Y", "wt_npirc": "copy irc-formatted track info", "wt_nptxt": "copy plaintext track info", "wt_m3ua": "add to m3u playlist (click 📻copy later)", "wt_m3uc": "copy m3u playlist to clipboard", "wt_grid": "toggle grid / list view$NHotkey: G", "wt_prev": "previous track$NHotkey: J", "wt_play": "play / pause$NHotkey: P", "wt_next": "next track$NHotkey: L", "ul_par": "parallel uploads:", "ut_rand": "randomize filenames", "ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server\">📅", "ut_ow": "overwrite existing files on the server?$N🛡️: never (will generate a new filename instead)$N🕒: overwrite if server-file is older than yours$N♻️: always overwrite if the files are different$N⏭️: unconditionally skip all existing files", "ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck", "ut_ask": 'ask for confirmation before upload starts">💭', "ut_pot": "improve upload speed on slow devices$Nby making the UI less complex", "ut_srch": "don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)", "ut_par": "pause uploads by setting it to 0$N$Nincrease if your connection is slow / high latency$N$Nkeep it 1 on LAN or if the server HDD is a bottleneck", "ul_btn": "drop files / folders
    here (or click me)", "ul_btnu": "U P L O A D", "ul_btns": "S E A R C H", "ul_hash": "hash", "ul_send": "send", "ul_done": "done", "ul_idle1": "no uploads are queued yet", "ut_etah": "average <em>hashing</em> speed, and estimated time until finish", "ut_etau": "average <em>upload</em> speed and estimated time until finish", "ut_etat": "average <em>total</em> speed and estimated time until finish", "uct_ok": "completed successfully", "uct_ng": "no-good: failed / rejected / not-found", "uct_done": "ok and ng combined", "uct_bz": "hashing or uploading", "uct_q": "idle, pending", "utl_name": "filename", "utl_ulist": "list", "utl_ucopy": "copy", "utl_links": "links", "utl_stat": "status", "utl_prog": "progress", // keep short: "utl_404": "404", "utl_err": "ERROR", "utl_oserr": "OS-error", "utl_found": "found", "utl_defer": "defer", "utl_yolo": "YOLO", "utl_done": "done", "ul_flagblk": "the files were added to the queue
    however there is a busy up2k in another browser tab,
    so waiting for that to finish first", "ul_btnlk": "the server configuration has locked this switch into this state", "udt_up": "Upload", "udt_srch": "Search", "udt_drop": "drop it here", "u_nav_m": '
    aight, what do you have?
    Enter = Files (one or more)\nESC = One folder (including subfolders)', "u_nav_b": 'FilesOne folder', "cl_opts": "switches", "cl_hfsz": "filesize", "cl_themes": "theme", "cl_langs": "language", "cl_ziptype": "folder download", "cl_uopts": "up2k switches", "cl_favico": "favicon", "cl_bigdir": "big dirs", "cl_hsort": "#sort", "cl_keytype": "key notation", "cl_hiddenc": "hidden columns", "cl_hidec": "hide", "cl_reset": "reset", "cl_hpick": "tap on column headers to hide in the table below", "cl_hcancel": "column hiding aborted", "cl_rcm": "right-click menu", "ct_grid": '田 the grid', "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel', "ct_dsel": 'use drag-selection in grid-view">dsel', "ct_dl": 'force download (don\'t display inline) when a file is clicked">dl', "ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯', "ct_dots": 'show hidden files (if server permits)">dotfiles', "ct_qdel": 'when deleting files, only ask for confirmation once">qdel', "ct_dir1st": 'sort folders before files">📁 first', "ct_nsort": 'natural sort (for filenames with leading digits)">nsort', "ct_utc": 'show all datetimes in UTC">UTC', "ct_readme": 'show README.md in folder listings">📜 readme', "ct_idxh": 'show index.html instead of folder listing">htm', "ct_sbars": 'show scrollbars">⟊', "cut_umod": "if a file already exists on the server, update the server's last-modified timestamp to match your local file (requires write+delete permissions)\">re📅", "cut_turbo": "the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple "does this have the same filesize on the server?" so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then "upload" the same files again to let the client verify them\">turbo", "cut_datechk": "has no effect unless the turbo button is enabled$N$Nreduces the yolo factor by a tiny amount; checks whether the file timestamps on the server matches yours$N$Nshould theoretically catch most unfinished / corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards\">date-chk", "cut_u2sz": "size (in MiB) of each upload chunk; big values fly better across the atlantic. Try low values on very unreliable connections", "cut_flag": "ensure only one tab is uploading at a time $N -- other tabs must have this enabled too $N -- only affects tabs on the same domain", "cut_az": "upload files in alphabetical order, rather than smallest-file-first$N$Nalphabetical order can make it easier to eyeball if something went wrong on the server, but it makes uploading slightly slower on fiber / LAN", "cut_nag": "OS notification when upload completes$N(only if the browser or tab is not active)", "cut_sfx": "audible alert when upload completes$N(only if the browser or tab is not active)", "cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$Nmakes https 30% faster, http 4.5x faster\">mt", "cut_wasm": "use wasm instead of the browser's built-in hasher; improves speed on chrome-based browsers but increases CPU load, and many older versions of chrome have bugs which makes the browser consume all RAM and crash if this is enabled\">wasm", "cft_text": "favicon text (blank and refresh to disable)", "cft_fg": "foreground color", "cft_bg": "background color", "cdt_lim": "max number of files to show in a folder", "cdt_ask": "when scrolling to the bottom,$Ninstead of loading more files,$Nask what to do", "cdt_hsort": "`how many sorting rules (`,sorthref`) to include in media-URLs. Setting this to 0 will also ignore sorting-rules included in media links when clicking them", "cdt_ren": "enable custom right-click menu, you can still access the regular menu by pressing the shift key and right-clicking\">enable", "cdt_rdb": "show the regular right-click menu when the custom one is already open and right-clicking again\">double", "tt_entree": "show navpane (directory tree sidebar)$NHotkey: B", "tt_detree": "show breadcrumbs$NHotkey: B", "tt_visdir": "scroll to selected folder", "tt_ftree": "toggle folder-tree / textfiles$NHotkey: V", "tt_pdock": "show parent folders in a docked pane at the top", "tt_dynt": "autogrow as tree expands", "tt_wrap": "word wrap", "tt_hover": "reveal overflowing lines on hover$N( breaks scrolling unless mouse $N  cursor is in the left gutter )", "ml_pmode": "at end of folder...", "ml_btns": "cmds", "ml_tcode": "transcode", "ml_tcode2": "transcode to", "ml_tint": "tint", "ml_eq": "audio equalizer", "ml_drc": "dynamic range compressor", "ml_ss": "skip silence", "mt_loop": "loop/repeat one song\">🔁", "mt_one": "stop after one song\">1️⃣", "mt_shuf": "shuffle the songs in each folder\">🔀", "mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶", "mt_preload": "start loading the next song near the end for gapless playback\">preload", "mt_prescan": "go to the next folder before the last song$Nends, keeping the webbrowser happy$Nso it doesn't stop the playback\">nav", "mt_fullpre": "try to preload the entire song;$N✅ enable on unreliable connections,$N❌ disable on slow connections probably\">full", "mt_fau": "on phones, prevent music from stopping if the next song doesn't preload fast enough (can make tags display glitchy)\">☕️", "mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s", "mt_npclip": "show buttons for clipboarding the currently playing song\">/np", "mt_m3u_c": "show buttons for clipboarding the$Nselected songs as m3u8 playlist entries\">📻", "mt_octl": "os integration (media hotkeys / osd)\">os-ctl", "mt_oseek": "allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\">seek", "mt_oscv": "show album cover in osd\">art", "mt_follow": "keep the playing track scrolled into view\">🎯", "mt_compact": "compact controls\">⟎", "mt_uncache": "clear cache  (try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache", "mt_mloop": "loop the open folder\">🔁 loop", "mt_mnext": "load the next folder and continue\">📂 next", "mt_mstop": "stop playback\">⏸ stop", "mt_cflac": "convert flac / wav to {0}\">flac", "mt_caac": "convert aac / m4a to {0}\">aac", "mt_coth": "convert all others (not mp3) to {0}\">oth", "mt_c2opus": "best choice for desktops, laptops, android\">opus", "mt_c2owa": "opus-weba, for iOS 17.5 and newer\">owa", "mt_c2caf": "opus-caf, for iOS 11 through 17\">caf", "mt_c2mp3": "use this on very old devices\">mp3", "mt_c2flac": "best sound quality, but huge downloads\">flac", "mt_c2wav": "uncompressed playback (even bigger)\">wav", "mt_c2ok": "nice, good choice", "mt_c2nd": "that's not the recommended output format for your device, but that's fine", "mt_c2ng": "your device does not seem to support this output format, but let's try anyways", "mt_xowa": "there are bugs in iOS preventing background playback using this format; please use caf or mp3 instead", "mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting", "mt_eq": "`enables the equalizer and gain control;$N$Nboost `0` = standard 100% volume (unmodified)$N$Nwidth `1  ` = standard stereo (unmodified)$Nwidth `0.5` = 50% left-right crossfeed$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that", "mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of 'tresh' -24 and 'ratio' 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90 (only works in firefox; RLS is max 1 in other browsers)$N$N(see wikipedia, they explain it much better)", "mt_ss": "`enables skip-silence; multiplies playback speed by `ffwd` near the start/end of songs when volume is under `vol` and the playback position is within the first `start`% or the last `end`% of the track", "mt_ssvt": "volume threshold (0-255)\">vol", "mt_ssts": "active threshold (% of track, start)\">start", "mt_sste": "active threshold (% of track, end)\">end", "mt_sssm": "playback speed multiplier (range: 0.15 to 8)\">ffwd", "mb_play": "play", "mm_hashplay": "play this audio file?", "mm_m3u": "press Enter/OK to Play\npress ESC/Cancel to Edit", "mp_breq": "need firefox 82+ or chrome 73+ or iOS 15+", "mm_bload": "now loading...", "mm_bconv": "converting to {0}, please wait...", "mm_opusen": "your browser cannot play aac / m4a files;\ntranscoding to opus is now enabled", "mm_playerr": "playback failed: ", "mm_eabrt": "The playback attempt was cancelled", "mm_enet": "Your internet connection is wonky", "mm_edec": "This file is supposedly corrupted??", "mm_esupp": "Your browser does not understand this audio format", "mm_eunk": "Unknown Errol", "mm_e404": "Could not play audio; error 404: File not found.", "mm_e403": "Could not play audio; error 403: Access denied.\n\nTry pressing F5 to reload, maybe you got logged out", "mm_e415": "Could not play audio; error 415: File transcoding failed; check server logs.", "mm_e500": "Could not play audio; error 500: Check server logs.", "mm_e5xx": "Could not play audio; server error ", "mm_nof": "not finding any more audio files nearby", "mm_prescan": "Looking for music to play next...", "mm_scank": "Found the next song:", "mm_uncache": "cache cleared; all songs will redownload on next playback", "mm_hnf": "that song no longer exists", "im_hnf": "that image no longer exists", "f_empty": 'this folder is empty', "f_chide": 'this will hide the column «{0}»\n\nyou can unhide columns in the settings tab', "f_bigtxt": "this file is {0} MiB large -- really view as text?", "f_bigtxt2": "view just the end of the file instead? this will also enable following/tailing, showing newly added lines of text in real time", "fbd_more": '
    showing {0} of {1} files; show {2} or show all
    ', "fbd_all": '
    showing {0} of {1} files; show all
    ', "f_anota": "only {0} of the {1} items were selected;\nto select the full folder, first scroll to the bottom", "f_dls": 'the file links in the current folder have\nbeen changed into download links', "f_dl_nd": 'skipping folder (use zip/tar download instead):\n', "f_partial": "To safely download a file which is currently being uploaded, please click the file which has the same filename, but without the .PARTIAL file extension. Please press CANCEL or Escape to do this.\n\nPressing OK / Enter will ignore this warning and continue downloading the .PARTIAL scratchfile instead, which will almost definitely give you corrupted data.", "ft_paste": "paste {0} items$NHotkey: ctrl-V", "fr_eperm": 'cannot rename:\nyou do not have “move” permission in this folder', "fd_eperm": 'cannot delete:\nyou do not have “delete” permission in this folder', "fc_eperm": 'cannot cut:\nyou do not have “move” permission in this folder', "fp_eperm": 'cannot paste:\nyou do not have “write” permission in this folder', "fr_emore": "select at least one item to rename", "fd_emore": "select at least one item to delete", "fc_emore": "select at least one item to cut", "fcp_emore": "select at least one item to copy to clipboard", "fs_sc": "share the folder you're in", "fs_ss": "share the selected files", "fs_just1d": "you cannot select more than one folder,\nor mix files and folders in one selection", "fs_abrt": "❌ abort", "fs_rand": "🎲 rand.name", "fs_go": "✅ create share", "fs_name": "name", "fs_src": "source", "fs_pwd": "passwd", "fs_exp": "expiry", "fs_tmin": "min", "fs_thrs": "hours", "fs_tdays": "days", "fs_never": "eternal", "fs_pname": "optional link name; will be random if blank", "fs_tsrc": "the file or folder to share", "fs_ppwd": "optional password", "fs_w8": "creating share...", "fs_ok": "press Enter/OK to Clipboard\npress ESC/Cancel to Close", "frt_dec": "may fix some cases of broken filenames\">url-decode", "frt_rst": "reset modified filenames back to the original ones\">↺ reset", "frt_abrt": "abort and close this window\">❌ cancel", "frb_apply": "APPLY RENAME", "fr_adv": "batch / metadata / pattern renaming\">advanced", "fr_case": "case-sensitive regex\">case", "fr_win": "windows-safe names; replace <>:"\\|?* with japanese fullwidth characters\">win", "fr_slash": "replace / with a character that doesn't cause new folders to be created\">no /", "fr_re": "`regex search pattern to apply to original filenames; capturing groups can be referenced in the format field below like `(1)` and `(2)` and so on", "fr_fmt": "`inspired by foobar2000:$N`(title)` is replaced by song title,$N`[(artist) - ](title)` skips [this] part if artist is blank$N`$lpad((tn),2,0)` pads tracknumber to 2 digits", "fr_pdel": "delete", "fr_pnew": "save as", "fr_pname": "provide a name for your new preset", "fr_aborted": "aborted", "fr_lold": "old name", "fr_lnew": "new name", "fr_tags": "tags for the selected files (read-only, just for reference):", "fr_busy": "renaming {0} items...\n\n{1}", "fr_efail": "rename failed:\n", "fr_nchg": "{0} of the new names were altered due to win and/or no /\n\nOK to continue with these altered new names?", "fd_ok": "delete OK", "fd_err": "delete failed:\n", "fd_none": "nothing was deleted; maybe blocked by server config (xbd)?", "fd_busy": "deleting {0} items...\n\n{1}", "fd_warn1": "DELETE these {0} items?", "fd_warn2": "Last chance! No way to undo. Delete?", "fc_ok": "cut {0} items", "fc_warn": 'cut {0} items\n\nbut: only this browser-tab can paste them\n(since the selection is so absolutely massive)', "fcc_ok": "copied {0} items to clipboard", "fcc_warn": 'copied {0} items to clipboard\n\nbut: only this browser-tab can paste them\n(since the selection is so absolutely massive)', "fp_apply": "use these names", "fp_skip": "skip conflicts", // TLNote: "skip existing names" (filenames taken in target folder) "fp_ecut": "first cut or copy some files / folders to paste / move\n\nnote: you can cut / paste across different browser tabs", "fp_ename": "{0} items cannot be moved here because the names are already taken. Give them new names below to continue, or blank the name (\"skip conflicts\") to skip them:", "fcp_ename": "{0} items cannot be copied here because the names are already taken. Give them new names below to continue, or blank the name (\"skip conflicts\") to skip them:", "fp_emore": "there are still some filename collisions left to fix", "fp_ok": "move OK", "fcp_ok": "copy OK", "fp_busy": "moving {0} items...\n\n{1}", "fcp_busy": "copying {0} items...\n\n{1}", "fp_abrt": "aborting...", "fp_err": "move failed:\n", "fcp_err": "copy failed:\n", "fp_confirm": "move these {0} items here?", "fcp_confirm": "copy these {0} items here?", "fp_etab": 'failed to read clipboard from other browser tab', "fp_name": "uploading a file from your device. Give it a name:", "fp_both_m": '
    choose what to paste
    Enter = Move {0} files from «{1}»\nESC = Upload {2} files from your device', "fcp_both_m": '
    choose what to paste
    Enter = Copy {0} files from «{1}»\nESC = Upload {2} files from your device', "fp_both_b": 'MoveUpload', "fcp_both_b": 'CopyUpload', "mk_noname": "type a name into the text field on the left before you do that :p", "nmd_i1": "also add the file extension you want, for example .md", "nmd_i2": "you can only create .{0} files because you don't have the delete-permission", "tv_load": "Loading text document:\n\n{0}\n\n{1}% ({2} of {3} MiB loaded)", "tv_xe1": "could not load textfile:\n\nerror ", "tv_xe2": "404, file not found", "tv_lst": "list of textfiles in", "tvt_close": "return to folder view$NHotkey: M (or Esc)\">❌ close", "tvt_dl": "download this file$NHotkey: Y\">💾 download", "tvt_prev": "show previous document$NHotkey: i\">⬆ prev", "tvt_next": "show next document$NHotkey: K\">⬇ next", "tvt_sel": "select file   ( for cut / copy / delete / ... )$NHotkey: S\">sel", "tvt_j": "beautify json$NHotkey: shift-J\">j", "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "tvt_tail": "monitor file for changes; show new lines in real time\">📡 follow", "tvt_wrap": "word-wrap\">↵", "tvt_atail": "lock scroll to bottom of page\">⚓", "tvt_ctail": "decode terminal colors (ansi escape codes)\">🌈", "tvt_ntail": "scrollback limit (how many bytes of text to keep loaded)", "m3u_add1": "song added to m3u playlist", "m3u_addn": "{0} songs added to m3u playlist", "m3u_clip": "m3u playlist now copied to clipboard\n\nyou should create a new textfile named something.m3u and paste the playlist in that document; this will make it playable", "gt_vau": "don't show videos, just play the audio\">🎧", "gt_msel": "enable file selection; ctrl-click a file to override$N$N<em>when active: doubleclick a file / folder to open it</em>$N$NHotkey: S\">multiselect", "gt_crop": "center-crop thumbnails\">crop", "gt_3x": "hi-res thumbnails\">3x", "gt_zoom": "zoom", "gt_chop": "chop", "gt_sort": "sort by", "gt_name": "name", "gt_sz": "size", "gt_ts": "date", "gt_ext": "type", "gt_c1": "truncate filenames more (show less)", "gt_c2": "truncate filenames less (show more)", "sm_w8": "searching...", "sm_prev": "search results below are from a previous query:\n ", "sl_close": "close search results", "sl_hits": "showing {0} hits", "sl_moar": "load more", "s_sz": "size", "s_dt": "date", "s_rd": "path", "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": "uploaded after", "s_u2": "and/or before", "s_r1": "path contains   (space-separated)", "s_f1": "name contains   (negate with -nope)", "s_t1": "tags contains   (^=start, end=$)", "s_a1": "specific metadata properties", "md_eshow": "cannot render ", "md_off": "[📜readme] disabled in [⚙️] -- document hidden", "badreply": "Failed to parse reply from server", "xhr403": "403: Access denied\n\ntry pressing F5, maybe you got logged out", "xhr0": "unknown (probably lost connection to server, or server is offline)", "cf_ok": "sorry about that -- DD" + wah + "oS protection kicked in\n\nthings should resume in about 30 sec\n\nif nothing happens, hit F5 to reload the page", "tl_xe1": "could not list subfolders:\n\nerror ", "tl_xe2": "404: Folder not found", "fl_xe1": "could not list files in folder:\n\nerror ", "fl_xe2": "404: Folder not found", "fd_xe1": "could not create subfolder:\n\nerror ", "fd_xe2": "404: Parent folder not found", "fsm_xe1": "could not send message:\n\nerror ", "fsm_xe2": "404: Parent folder not found", "fu_xe1": "failed to load unpost list from server:\n\nerror ", "fu_xe2": "404: File not found??", "fz_tar": "uncompressed gnu-tar file (linux / mac)", "fz_pax": "uncompressed pax-format tar (slower)", "fz_targz": "gnu-tar with gzip level 3 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead", "fz_tarxz": "gnu-tar with xz level 1 compression$N$Nthis is usually very slow, so$Nuse uncompressed tar instead", "fz_zip8": "zip with utf8 filenames (maybe wonky on windows 7 and older)", "fz_zipd": "zip with traditional cp437 filenames, for really old software", "fz_zipc": "cp437 with crc32 computed early,$Nfor MS-DOS PKZIP v2.04g (october 1993)$N(takes longer to process before download can start)", "un_m1": "you can delete your recent uploads (or abort unfinished ones) below", "un_upd": "refresh", "un_m4": "or share the files visible below:", "un_ulist": "show", "un_ucopy": "copy", "un_flt": "optional filter:  URL must contain", "un_fclr": "clear filter", "un_derr": 'unpost-delete failed:\n', "un_f5": 'something broke, please try a refresh or hit F5', "un_uf5": "sorry but you have to refresh the page (for example by pressing F5 or CTRL-R) before this upload can be aborted", "un_nou": 'warning: server too busy to show unfinished uploads; click the "refresh" link in a bit', "un_noc": 'warning: unpost of fully uploaded files is not enabled/permitted in server config', "un_max": "showing first 2000 files (use the filter)", "un_avail": "{0} recent uploads can be deleted
    {1} unfinished ones can be aborted", "un_m2": "sorted by upload time; most recent first:", "un_no1": "sike! no uploads are sufficiently recent", "un_no2": "sike! no uploads matching that filter are sufficiently recent", "un_next": "delete the next {0} files below", "un_abrt": "abort", "un_del": "delete", "un_m3": "loading your recent uploads...", "un_busy": "deleting {0} files...", "un_clip": "{0} links copied to clipboard", "u_https1": "you should", "u_https2": "switch to https", "u_https3": "for better performance", "u_ancient": 'your browser is impressively ancient -- maybe you should use bup instead', "u_nowork": "need firefox 53+ or chrome 57+ or iOS 11+", "tail_2old": "need firefox 105+ or chrome 71+ or iOS 14.5+", "u_nodrop": 'your browser is too old for drag-and-drop uploading', "u_notdir": "that's not a folder!\n\nyour browser is too old,\nplease try dragdrop instead", "u_uri": "to dragdrop images from other browser windows,\nplease drop it onto the big upload button", "u_enpot": 'switch to potato UI (may improve upload speed)', "u_depot": 'switch to fancy UI (may reduce upload speed)', "u_gotpot": 'switching to the potato UI for improved upload speed,\n\nfeel free to disagree and switch back!', "u_pott": "

    files:   {0} finished,   {1} failed,   {2} busy,   {3} queued

    ", "u_ever": "this is the basic uploader; up2k needs at least
    chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'this is the basic uploader; up2k is better', "u_uput": 'optimize for speed (skip checksum)', "u_ewrite": 'you do not have write-access to this folder', "u_eread": 'you do not have read-access to this folder', "u_enoi": 'file-search is not enabled in server config', "u_enoow": "overwrite will not work here; need Delete-permission", "u_badf": 'These {0} files (of {1} total) were skipped, possibly due to filesystem permissions:\n\n', "u_blankf": 'These {0} files (of {1} total) are blank / empty; upload them anyways?\n\n', "u_applef": 'These {0} files (of {1} total) are probably undesirable;\nPress OK/Enter to SKIP the following files,\nPress Cancel/ESC to NOT exclude, and UPLOAD those as well:\n\n', "u_just1": '\nMaybe it works better if you select just one file', "u_ff_many": "if you're using Linux / MacOS / Android, then this amount of files may crash Firefox!\nif that happens, please try again (or use Chrome).", "u_up_life": "This upload will be deleted from the server\n{0} after it completes", "u_asku": 'upload these {0} files to {1}', "u_unpt": "you can undo / delete this upload using the top-left 🧯", "u_bigtab": 'about to show {0} files\n\nthis may crash your browser, are you sure?', "u_scan": 'Scanning files...', "u_dirstuck": 'directory iterator got stuck trying to access the following {0} items; will skip:', "u_etadone": 'Done ({0}, {1} files)', "u_etaprep": '(preparing to upload)', "u_hashdone": 'hashing done', "u_hashing": 'hash', "u_hs": 'handshaking...', "u_started": "the files are now being uploaded; see [🚀]", "u_dupdefer": "duplicate; will be processed after all other files", "u_actx": "click this text to prevent loss of
    performance when switching to other windows/tabs", "u_fixed": "OK!  Fixed it 👍", "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": "will retry; see bottom-right", "u_ehsfin": "server rejected the request to finalize upload; retrying...", "u_ehssrch": "server rejected the request to perform search; retrying...", "u_ehsinit": "server rejected the request to initiate upload; retrying...", "u_eneths": "network error while performing upload handshake; retrying...", "u_enethd": "network error while testing target existence; retrying...", "u_cbusy": "waiting for server to trust us again after a network glitch...", "u_ehsdf": "server ran out of disk space!\n\nwill keep retrying, in case someone\nfrees up enough space to continue", "u_emtleak1": "it looks like your webbrowser may have a memory leak;\nplease", "u_emtleak2": ' switch to https (recommended) or ', "u_emtleak3": ' ', "u_emtleakc": 'try the following:\n
    • hit F5 to refresh the page
    • then disable the  mt  button in the  ⚙️ settings
    • and try that upload again
    Uploads will be a bit slower, but oh well.\nSorry for the trouble !\n\nPS: chrome v107 has a bugfix for this', "u_emtleakf": 'try the following:\n
    • hit F5 to refresh the page
    • then enable 🥔 (potato) in the upload UI
    • and try that upload again
    \nPS: firefox will hopefully have a bugfix at some point', "u_s404": "not found on server", "u_expl": "explain", "u_maxconn": "most browsers limit this to 6, but firefox lets you raise it with connections-per-server in about:config", "u_tu": '

    WARNING: turbo enabled,  client may not detect and resume incomplete uploads; see turbo-button tooltip

    ', "u_ts": '

    WARNING: turbo enabled,  search results can be incorrect; see turbo-button tooltip

    ', "u_turbo_c": "turbo is disabled in server config", "u_turbo_g": "disabling turbo because you don't have\ndirectory listing privileges within this volume", "u_life_cfg": 'autodelete after min (or hours)', "u_life_est": 'upload will be deleted ---', "u_life_max": 'this folder enforces a\nmax lifetime of {0}', "u_unp_ok": 'unpost is allowed for {0}', "u_unp_ng": 'unpost will NOT be allowed', "ue_ro": 'your access to this folder is Read-Only\n\n', "ue_nl": 'you are currently not logged in', "ue_la": 'you are currently logged in as "{0}"', "ue_sr": 'you are currently in file-search mode\n\nswitch to upload-mode by clicking the magnifying glass 🔎 (next to the big SEARCH button), and try uploading again\n\nsorry', "ue_ta": 'try uploading again, it should work now', "ue_ab": "this file is already being uploaded into another folder, and that upload must be completed before the file can be uploaded elsewhere.\n\nYou can abort and forget the initial upload using the top-left 🧯", "ur_1uo": "OK: File uploaded successfully", "ur_auo": "OK: All {0} files uploaded successfully", "ur_1so": "OK: File found on server", "ur_aso": "OK: All {0} files found on server", "ur_1un": "Upload failed, sorry", "ur_aun": "All {0} uploads failed, sorry", "ur_1sn": "File was NOT found on server", "ur_asn": "The {0} files were NOT found on server", "ur_um": "Finished;\n{0} uploads OK,\n{1} uploads failed, sorry", "ur_sm": "Finished;\n{0} files found on server,\n{1} files NOT found on server", "rc_opn": "open", "rc_ply": "play", "rc_pla": "play as audio", "rc_txt": "open in textfile viewer", "rc_md": "open in markdown viewer", "rc_dl": "download", "rc_zip": "download as archive", "rc_cpl": "copy link", "rc_del": "delete", "rc_cut": "cut", "rc_cpy": "copy", "rc_pst": "paste", "rc_rnm": "rename", "rc_nfo": "new folder", "rc_nfi": "new file", "rc_sal": "select all", "rc_sin": "invert selection", "rc_shf": "share this folder", "rc_shs": "share selection", "lang_set": "refresh to make the change take effect?", }; var LANGN = [ ["eng", "English"], ["nor", "Norsk"], ["chi", "中文"], ["cze", "Čeština"], ["deu", "Deutsch"], ["epo", "Esperanto"], ["fin", "Suomi"], ["fra", "français"], ["grc", "Ελληνικά"], ["hun", "Magyar"], ["ita", "Italiano"], ["jpn", "日本語"], ["kor", "한국어"], ["nld", "Nederlands"], ["nno", "Nynorsk"], ["pol", "Polski"], ["por", "Português"], ["rus", "Русский"], ["spa", "Español"], ["swe", "Svenska"], ["tur", "Türkçe"], ["ukr", "Українська"], ["vie", "Tiếng Việt"], ]; if (window.langmod) langmod(); var L = Ls[lang] || Ls.eng, LANGS = []; for (var a = 0; a < LANGN.length; a++) LANGS.push(LANGN[a][0]); function langtest() { var n = LANGS.length - 1; for (var a = 1; a < LANGS.length; a++) import_js(SR + '/.cpr/w/tl/' + LANGS[a] + '.js', function () { if (!--n) langtest2(); }); } function langtest2() { for (var a = 0; a < LANGS.length; a++) { for (var b = a + 1; b < LANGS.length; b++) { var i1 = Object.keys(Ls[LANGS[a]]).length > Object.keys(Ls[LANGS[b]]).length ? a : b, i2 = i1 == a ? b : a, t1 = Ls[LANGS[i1]], t2 = Ls[LANGS[i2]]; for (var k in t1) if (!t2[k] && !/^ht_.5$/.test(k)) { console.log("E missing TL", LANGS[i2], k); t2[k] = t1[k]; } } } } if (!Ls[lang]) alert('unsupported --lang "' + lang + '" specified in server args;\nplease use one of these: ' + LANGS); modal.load(); // toolbar ebi('ops').innerHTML = ( '--' + '🔎' + (have_del ? '🧯' : '') + '🚀' + '🎈' + '📂' + '📝' + '📟' + '🎺' + '⚙️' + (IE ? '' + L.ot_noie + '' : '') + '
    ' ); // media player ebi('widget').innerHTML = ( '
    ' + '' + '📨sharenamedel.cutcopy📋paste' + '📦zip' + 'sel.
    allsel.
    inv.zipdl' + '
    📋irc📋txt' + '📻add📻copy' + '♫' + '
    ' + '
    ' + ' ' + ' ' + ' ' + ' ' + '
    ' + '
    ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '
    ' ); // up2k ui ebi('op_up2k').innerHTML = ( '
    \n' + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '

    ' + L.ul_par + '
    \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '
    \n' + ' +
     \n' + '
    \n' + '
    \n' + '
    \n' + '
    \n' + ' \n' + L.ul_btn + '
    \n' + '
    \n' + '
    \n' + '
    \n' + L.ul_hash + ': (' + L.ul_idle1 + ')
    \n' + L.ul_send + ': (' + L.ul_idle1 + ')
    \n' + '
    ' + L.ul_done + ': (' + L.ul_idle1 + ')\n' + '
    \n' + '
    \n' + ' ok 0ng 0done 0busy 0que 0\n' + '
    \n' + '
    \n' + '
    \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '
    ' + L.utl_name + '  (' + L.utl_ulist + '/' + L.utl_ucopy + '' + L.utl_links + ')' + L.utl_stat + '' + L.utl_prog + '
    \n' + '

    ' + L.ul_flagblk + '

    \n' + '
    ' + '
    ' ); ebi('wrap').insertBefore(mknod('div', 'lazy'), ebi('epi')); var x = ebi('bbsw'); x.parentNode.insertBefore(mknod('div', null, ''), x); (function () { var o = mknod('div'); o.innerHTML = ( '
    \n' + '
    🚀 ' + L.udt_up + '
    🚀' + L.udt_up + '
    ' + L.udt_up + '🚀
    \n' + '
    🔎 ' + L.udt_srch + '
    🔎' + L.udt_srch + '
    ' + L.udt_srch + '🔎
    \n' + '
    \n' + '
    \n' + '
    ' ); document.body.appendChild(o); })(); // config panel ebi('op_cfg').innerHTML = ( '\n' + '
    \n' + '

    ' + L.cl_themes + '

    \n' + '
    \n' + '
    \n' + '
    \n' + '
    \n' + '

    ' + L.cl_langs + '

    \n' + '
    \n' + '
    \n' + (have_zip ? ( '

    ' + L.cl_ziptype + '

    \n' ) : '') + '
    \n' + '

    ' + L.cl_uopts + '

    \n' + '
    \n' + ' ' + ' 💤\n' + ' az\n' + ' 🔔\n' + ' 🔊\n' + ' \n' + '
    \n' + '
    \n' + '
    \n' + '

    ' + L.cl_favico + ' 🎉

    \n' + '
    \n' + ' ' + ' ' + ' ' + ' \n' + '
    \n' + '
    \n' + '
    \n' + '

    ' + L.cl_bigdir + '

    \n' + '
    \n' + ' ' + ' ask\n' + ' \n' + '
    \n' + '
    \n' + '
    \n' + '

    ' + L.cl_hsort + '

    \n' + '
    \n' + ' ' + ' \n' + '
    \n' + '
    \n' + '

    ' + L.cl_keytype + '

    \n' + (!MOBILE ? '

    ' + L.cl_rcm + '

    ' ); // navpane ebi('tree').innerHTML = ( '
    \n' + ' 🍞...\n' + ' +\n' + ' \n' + ' 🎯\n' + ' 📃\n' + ' 📌\n' + ' a\n' + ' \n' + ' 👀\n' + '
    \n' + '
      \n' + '
        \n' + '
          \n' + '
           
          ' ); clmod(ebi('tree'), 'sbar', 1); ebi('entree').setAttribute('tt', L.tt_entree); ebi('goh').textContent = L.goh; QS('#op_mkdir input[type="submit"]').value = L.ab_mkdir; QS('#op_new_md input[type="submit"]').value = L.ab_mkdoc; QS('#op_msg input[type="submit"]').value = L.ab_msg; // right-click menu ebi('rcm').innerHTML = ( '' + L.rc_opn + '' + '' + L.rc_ply + '' + '' + L.rc_pla + '' + '' + L.rc_txt + '' + '' + L.rc_md + '' + '
          ' + '' + L.rc_cpl + '' + '' + L.rc_dl + '' + (have_zip ? '' + L.rc_zip + '' : '') + '
          ' + (have_del ? '' + L.rc_del + '' : '') + (have_mv ? '' + L.rc_cut + '' : '') + '' + L.rc_cpy + '' + (has(perms, "write") ? '' + L.rc_pst + '' + (have_mv ? '' + L.rc_rnm + '' : '') + '
          ' + '' + L.rc_nfo + '' + '' + L.rc_nfi + '' : '') + '
          ' + '' + L.rc_sal + '' + '' + L.rc_sin + '' + '' ); (function () { var ops = QSA('#ops>a'); for (var a = 0; a < ops.length; a++) { ops[a].onclick = opclick; var v = ops[a].getAttribute('data-dest'); if (v) ops[a].href = '#v=' + v; } })(); function opclick(e) { var dest = this.getAttribute('data-dest'); if (QS('#op_' + dest + '.act')) dest = ''; swrite('opmode', dest || null); if (ctrl(e)) return; ev(e); goto(dest); var input = QS('.opview.act input:not([type="hidden"])') if (input && !TOUCH) { tt.skip = true; input.focus(); } } function goto(dest) { var obj = QSA('.opview.act'); for (var a = obj.length - 1; a >= 0; a--) clmod(obj[a], 'act'); obj = QSA('#ops>a'); for (var a = obj.length - 1; a >= 0; a--) clmod(obj[a], 'act'); if (dest) { var lnk = QS('#ops>a[data-dest=' + dest + ']'), nps = lnk.getAttribute('data-perm'); nps = nps && nps.length ? nps.split(' ') : []; if (perms.length) for (var a = 0; a < nps.length; a++) if (!has(perms, nps[a])) return; if (!has(perms, 'read') && !has(perms, 'write') && (dest == 'up2k')) return; clmod(ebi('op_' + dest), 'act', 1); clmod(lnk, 'act', 1); var fn = window['goto_' + dest]; if (fn) fn(); } clmod(document.documentElement, 'op_open', dest); if (treectl) treectl.onscroll(); } var m = SPINNER.split(','), SPINNER_CSS = SPINNER.slice(1 + m[0].length); SPINNER = m[0]; var SBW, SBH; // scrollbar size function read_sbw() { var el = mknod('div'); el.style.cssText = 'overflow:scroll;width:100px;height:100px;position:absolute;top:0;left:0'; document.body.appendChild(el); SBW = el.offsetWidth - el.clientWidth; SBH = el.offsetHeight - el.clientHeight; document.body.removeChild(el); setcvar('--sbw', SBW + 'px'); setcvar('--sbh', SBH + 'px'); } onresize100.add(read_sbw, true); function check_image_support(format, uri) { var cached = window['have_' + format] = sread('have_' + format); if (cached !== null) return; var img = new Image(); img.onload = function () { window['have_' + format] = img.width > 0 && img.height > 0; swrite('have_' + format, 'ya'); }; img.onerror = function () { window['have_' + format] = false; swrite('have_' + format, ''); }; img.src = uri; } check_image_support('webp', "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=="); check_image_support('jxl', "data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA="); var img_re = APPLE ? /\.(a?png|avif|bmp|gif|hei[cf]s?|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i : /\.(a?png|avif|bmp|gif|jpe?g|jfif|svg|webp|webm|mkv|mp4|m4v|mov)(\?|$)/i; function set_files_html(html) { var files = ebi('files'); try { files.innerHTML = html; return files; } catch (e) { var par = files.parentNode; par.removeChild(files); files = mknod('div'); files.innerHTML = '' + html + '
          '; par.insertBefore(files.childNodes[0], ebi('lazy')); files = ebi('files'); return files; } } // actx breaks background album playback on ios var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext), ACB = sread('au_cbv') || 1, hash0 = location.hash, sloc0 = '' + location, noih = /[?&]v\b/.exec(sloc0), fullui = /[?&]fullui\b/.exec(sloc0), nonav = !fullui && (/[?&]nonav\b/.exec(sloc0) || window.ui_nonav), notree = !fullui && (/[?&]notree\b/.exec(sloc0) || window.ui_notree || nonav), dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0), abrt_key = "", can_shr = false, in_shr = false, rtt = null, srvinf = "", ldks = [], dks = {}, dk, mp; var x = ''; if (!fullui) { if (window.ui_nombar || /[?&]nombar\b/.exec(sloc0)) x += '#ops,'; if (window.ui_noacci || /[?&]noacci\b/.exec(sloc0)) x += '#acc_info,'; if (window.ui_nosrvi || /[?&]nosrvi\b/.exec(sloc0)) x += '#srv_info,#srv_info2,'; if (window.ui_nocpla || /[?&]nocpla\b/.exec(sloc0)) x += '#goh,'; if (window.ui_nolbar || /[?&]nolbar\b/.exec(sloc0)) x += '#wfp,'; if (window.ui_noctxb || /[?&]noctxb\b/.exec(sloc0)) x += '#wtoggle,'; if (window.ui_norepl || /[?&]norepl\b/.exec(sloc0)) x += '#repl,'; } if (x) document.head.appendChild(mknod('style', '', x.slice(0, -1) + '{display:none!important}')); if (location.pathname.indexOf('//') === 0) hist_replace(location.pathname.replace(/^\/+/, '/')); if (window.og_fn) { hash0 = 1; hist_replace(vsplit(get_evpath())[0]); } var hsortn = ebi('hsortn').value = icfg_get('hsortn', dhsortn); ebi('hsortn').oninput = function (e) { var n = parseInt(this.value); swrite('hsortn', hsortn = (isNum(n) ? n : dhsortn)); }; (function() { var args = ('' + hash0).split(/,sort/g); if (args.length < 2) return; var ret = []; for (var a = 1; a < args.length; a++) { var t = '', n = 1, z = args[a].split(',')[0]; if (z.startsWith('-')) { z = z.slice(1); n = -1; } if (z == "sz" || z.indexOf('/.') + 1) t = "int"; ret.push([z, n, t]); } n = Math.min(ret.length, hsortn); if (n) { var cmp = jread('fsort', []); if (JSON.stringify(ret.slice(0, n) != JSON.stringify(cmp.slice(0, n)))) jwrite('fsort', ret); } })(); var mpl = (function () { var have_mctl = 'mediaSession' in navigator && window.MediaMetadata; ebi('op_player').innerHTML = ( '

          ' + L.cl_opts + '

          ' + '

          ' + L.ml_drc + '

          ' + '

          ' + L.ml_eq + '

          ' + '

          ' + L.ml_ss + '

          ' + ''); var r = { "pb_mode": (sread('pb_mode', ['loop', 'next', 'stop']) || 'next').split('-')[0], "os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl, 'traversals': 0, 'm3ut': '#EXTM3U\n', 'np': [{'file': 'nothing'}, ['file']], }; bcfg_bind(r, 'one', 'au_one', false, function (v) { if (mp.au) mp.au.loop = !v && r.loop; }); bcfg_bind(r, 'loop', 'au_loop', false, function (v) { if (mp.au) mp.au.loop = v; }); bcfg_bind(r, 'shuf', 'au_shuf', false, function () { mp.read_order(); // don't bind }); bcfg_bind(r, 'aplay', 'au_aplay', true); bcfg_bind(r, 'preload', 'au_preload', true); bcfg_bind(r, 'prescan', 'au_prescan', true); bcfg_bind(r, 'fullpre', 'au_fullpre', false); bcfg_bind(r, 'fau', 'au_fau', MOBILE && !IPHONE, function (v) { mp.nopause(); if (mp.fau) { mp.fau.pause(); mp.fau = mpo.fau = null; console.log('stop fau'); } mp.init_fau(); }); bcfg_bind(r, 'waves', 'au_waves', true, function (v) { if (!v) pbar.unwave(); }); bcfg_bind(r, 'os_seek', 'au_os_seek', !IPHONE, announce); bcfg_bind(r, 'osd_cv', 'au_osd_cv', true, announce); bcfg_bind(r, 'clip', 'au_npclip', false, function (v) { clmod(ebi('wtoggle'), 'np', v && mp.au); }); bcfg_bind(r, 'm3uen', 'au_m3u_c', false, function (v) { clmod(ebi('wtoggle'), 'm3u', v && (mp.au || msel.getsel().length)); }); bcfg_bind(r, 'follow', 'au_follow', false, setaufollow); bcfg_bind(r, 'ac_flac', 'ac_flac', true); bcfg_bind(r, 'ac_aac', 'ac_aac', false); bcfg_bind(r, 'ac_oth', 'ac_oth', true, reload_mp); if (!have_acode) r.ac_flac = r.ac_aac = r.ac_oth = false; if (IPHONE) { ebi('au_fullpre').style.display = 'none'; r.fullpre = false; } ebi('au_uncache').onclick = function (e) { ev(e); ACB = (Date.now() % 46656).toString(36); swrite('au_cbv', ACB); reload_mp(); toast.inf(5, L.mm_uncache); }; ebi('au_os_ctl').onclick = function (e) { ev(e); r.os_ctl = !r.os_ctl && have_mctl; bcfg_set('au_os_ctl', r.os_ctl); if (!have_mctl) toast.err(5, L.mp_breq); }; function draw_pb_mode() { var btns = QSA('#pb_mode>a'); for (var a = 0, aa = btns.length; a < aa; a++) { clmod(btns[a], 'on', btns[a].getAttribute("m") == r.pb_mode); btns[a].onclick = set_pb_mode; } } draw_pb_mode(); function set_pb_mode(e) { ev(e); r.pb_mode = this.getAttribute('m'); swrite('pb_mode', r.pb_mode); draw_pb_mode(); } function set_tint() { var tint = icfg_get('pb_tint', 0); if (!tint) ebi('barbuf').style.removeProperty('background'); else ebi('barbuf').style.background = 'rgba(126,163,75,' + (tint / 100.0) + ')'; } ebi('pb_tint').oninput = function (e) { swrite('pb_tint', this.value); set_tint(); }; set_tint(); r.acode = function (url) { var c = true, cs = url.split('?')[0]; if (!have_acode) c = false; else if (/\.(wav|flac)$/i.exec(cs)) c = r.ac_flac; else if (/\.(aac|m4[abr])$/i.exec(cs)) c = r.ac_aac; else if (/\.(oga|ogg|opus)$/i.exec(cs) && (!can_ogg || mpl.ac2 == 'mp3')) c = true; else if (re_au_native.exec(cs)) c = false; // allow flac->flac (bitstream fixup) if (!c) return url; return addq(url, 'th=' + r.ac2); }; r.set_ac2 = function () { r.init_ac2(this.getAttribute('id').split('ac2')[1]); }; r.init_ac2 = function (v) { if (!window.have_acode) { r.ac2 = 'opus'; return; } var dv = can_ogg ? 'opus' : can_caf ? 'caf' : 'mp3', fmts = ['opus', 'owa', 'caf', 'mp3', 'flac', 'wav'], btns = []; if (v === dv) toast.ok(5, L.mt_c2ok); else if (v) toast.inf(10, L.mt_c2nd); if ((v == 'opus' && !can_ogg) || (v == 'caf' && !can_caf) || (v == 'owa' && !can_owa) || (v == 'flac' && !can_flac)) toast.warn(15, L.mt_c2ng); if (v == 'owa' && IPHONE) toast.err(30, L.mt_xowa); for (var a = 0; a < fmts.length; a++) { var btn = ebi('ac2' + fmts[a]); if (!btn) return console.log('!btn', fmts[a]); btn.onclick = r.set_ac2; btns.push(btn); } if (!IPHONE) btns[1].style.display = btns[2].style.display = 'none'; btns[4].style.display = have_c2flac ? '' : 'none'; btns[5].style.display = have_c2wav ? '' : 'none'; if (v) swrite('acode2', v); else v = dv; v = sread('acode2', fmts) || v; for (var a = 0; a < fmts.length; a++) clmod(btns[a], 'on', fmts[a] == v) r.ac2 = v; ebi('ac_flac').setAttribute('tt', L.mt_cflac.split('"')[0].format(v)); ebi('ac_aac').setAttribute('tt', L.mt_caac.split('"')[0].format(v)); ebi('ac_oth').setAttribute('tt', L.mt_coth.split('"')[0].format(v)); }; r.pp = function () { var adur, apos, playing = mp.au && !mp.au.paused; clearTimeout(mpl.t_eplay); clmod(ebi('np_inf'), 'playing', playing); if (mp.au && isNum(adur = mp.au.duration) && isNum(apos = mp.au.currentTime) && apos >= 0) ebi('np_pos').textContent = s2ms(apos); if (!r.os_ctl) return; navigator.mediaSession.playbackState = playing ? "playing" : "paused"; }; function setaufollow() { window[(r.follow ? "add" : "remove") + "EventListener"]("resize", scroll2playing); } setaufollow(); function announce() { if (!r.os_ctl || !mp.au) return; var np = mpl.np[0], fns = np.file.split(' - '), artist = (np.circle && np.circle != np.artist ? np.circle + ' // ' : '') + (np.artist || (fns.length > 1 ? fns[0] : '')), title = np.title || fns.pop(), cover = '', tags = { title: title }; if (artist) tags.artist = artist; if (np.album) tags.album = np.album; if (r.osd_cv) { var files = QSA("#files tr>td:nth-child(2)>a[id]"), cover = null; for (var a = 0, aa = files.length; a < aa; a++) { if (/^(cover|folder)\.(jpe?g|png|gif)$/i.test(files[a].textContent)) { cover = files[a].getAttribute('href'); break; } } cover = addq(cover || mp.au.osrc, 'th=j'); tags.artwork = [{ "src": cover, type: "image/jpeg" }]; } ebi('np_circle').textContent = np.circle || ''; ebi('np_album').textContent = np.album || ''; ebi('np_tn').textContent = np['.tn'] || ''; ebi('np_artist').textContent = np.artist || (fns.length > 1 ? fns[0] : ''); ebi('np_title').textContent = np.title || ''; ebi('np_dur').textContent = np['.dur'] || ''; ebi('np_url').textContent = uricom_dec(get_evpath()) + np.file.split('?')[0]; if (!MOBILE && cover) ebi('np_img').setAttribute('src', cover); else ebi('np_img').removeAttribute('src'); navigator.mediaSession.metadata = new MediaMetadata(tags); navigator.mediaSession.setActionHandler('play', mplay); navigator.mediaSession.setActionHandler('pause', mpause); navigator.mediaSession.setActionHandler('seekbackward', r.os_seek ? function () { seek_au_rel(-10); } : null); navigator.mediaSession.setActionHandler('seekforward', r.os_seek ? function () { seek_au_rel(10); } : null); navigator.mediaSession.setActionHandler('previoustrack', prev_song); navigator.mediaSession.setActionHandler('nexttrack', next_song); r.pp(); } r.announce = announce; r.stop = function () { if (!r.os_ctl) return; // dead code; left for debug navigator.mediaSession.metadata = null; navigator.mediaSession.playbackState = "paused"; var hs = 'play pause seekbackward seekforward previoustrack nexttrack'.split(/ /g); for (var a = 0; a < hs.length; a++) navigator.mediaSession.setActionHandler(hs[a], null); navigator.mediaSession.setPositionState(); }; r.unbuffer = function (url) { if (mp.au2 && (!url || mp.au2.rsrc == url)) { mp.au2.src = mp.au2.rsrc = ''; mp.au2.ld = 0; //owa mp.au2.load(); } if (!url) mpl.preload_url = null; } return r; })(); var za, can_ogg = true, can_owa = false, can_flac = false, can_caf = APPLE && !/ OS ([1-9]|1[01])_/.test(UA); try { za = new Audio(); can_ogg = za.canPlayType('audio/ogg; codecs=opus') === 'probably'; can_owa = za.canPlayType('audio/webm; codecs=opus') === 'probably'; can_flac = za.canPlayType('audio/flac') === 'probably'; can_caf = za.canPlayType('audio/x-caf') && can_caf; //'maybe' } catch (ex) { } za = null; if (can_owa && APPLE && / OS ([1-9]|1[0-7])_/.test(UA)) can_owa = false; mpl.init_ac2(); var re_m3u = /\.(m3u8?)$/i; var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4[abr]|mp3|oga|ogg|opus|wav)$/i : /\.(aac|flac|m4[abr]|mp3|wav)$/i, re_au_vid = /\.(3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i, re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4[abr]|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i; // extract songs + add play column var mpo = { "au": null, "au2": null, "acs": null, "fau": null }; function MPlayer() { var r = this; r.id = Date.now(); r.au = mpo.au; r.au2 = mpo.au2; r.acs = mpo.acs; r.fau = mpo.fau; r.tracks = {}; r.order = []; r.cd_pause = 0; var re_audio = have_acode && mpl.ac_oth ? re_au_all : re_au_native, trs = QSA('#files tbody tr'); for (var a = 0, aa = trs.length; a < aa; a++) { var tds = trs[a].getElementsByTagName('td'), link = tds[1].getElementsByTagName('a'); link = link[link.length - 1]; var url = link.getAttribute('href'), fn = url.split('?')[0]; if (re_audio.exec(fn)) { var tid = link.getAttribute('id'), txt = re_au_vid.exec(fn) ? '(🎧)' : L.mb_play; r.order.push(tid); r.tracks[tid] = url; tds[0].innerHTML = '
          ' + txt + ''; ebi('a' + tid).onclick = ev_play; clmod(trs[a], 'au', 1); } else if (re_m3u.exec(fn)) { var tid = link.getAttribute('id'); tds[0].innerHTML = '' + L.mb_play + ''; ebi('a' + tid).onclick = ev_load_m3u; } } r.vol = clamp(fcfg_get('vol', IPHONE ? 1 : dvol / 100), 0, 1); r.expvol = function (v) { return 0.5 * v + 0.5 * v * v; }; r.setvol = function (vol) { r.vol = clamp(vol, 0, 1); swrite('vol', vol); r.stopfade(true); if (r.au) r.au.volume = r.expvol(r.vol); }; r.shuffle = function () { if (!mpl.shuf) return; // durstenfeld for (var a = r.order.length - 1; a > 0; a--) { var b = Math.floor(Math.random() * (a + 1)), c = r.order[a]; r.order[a] = r.order[b]; r.order[b] = c; } }; r.shuffle(); r.read_order = function () { var order = [], links = QSA('#files>tbody>tr>td:nth-child(1)>a'); for (var a = 0, aa = links.length; a < aa; a++) { var tid = links[a].getAttribute('id'); if (!tid || tid.indexOf('af-') !== 0) continue; order.push(tid.slice(1)); } r.order = order; r.shuffle(); }; r.fdir = 0; r.fvol = -1; r.ftid = -1; r.ftimer = null; r.fade_in = function () { r.nopause(); start_actx(); r.fvol = 0; r.fdir = 0.025 * r.vol * (CHROME ? 1.5 : 1); if (r.au) { r.ftid = r.au.tid; r.au.play(); mpl.pp(); fader(); } }; r.fade_out = function () { mpss.stop(); r.fvol = r.vol; r.fdir = -0.05 * r.vol * (CHROME ? 2 : 1); r.ftid = r.au.tid; fader(); }; r.stopfade = function (hard) { clearTimeout(r.ftimer); if (hard) r.ftid = -1; } function fader() { r.stopfade(); if (!r.au || r.au.tid !== r.ftid) return; var done = true; r.fvol += r.fdir / (r.fdir < 0 && r.fvol < r.vol / 4 ? 2 : 1); if (r.fvol < 0) { r.fvol = 0; r.au.pause(); mpl.pp(); var t = r.au.currentTime - 0.8; if (isNum(t)) r.au.currentTime = Math.max(t, 0); } else if (r.fvol > r.vol) { r.fvol = r.vol; mpss.go(); } else done = false; r.au.volume = r.expvol(r.fvol); if (!done) setTimeout(fader, 10); } r.preload = function (url, full) { var t0 = Date.now(), fname = uricom_dec(url.split('/').pop().split('?')[0]); url = addq(mpl.acode(url), 'cache=987&_=' + ACB); mpl.preload_url = full ? url : null; if (mpl.waves) fetch(url.replace(/\bth=(opus|mp3)&/, '') + '&th=p').then(function (x) { x.body.getReader().read(); }); if (full) return fetch(url).then(function (x) { var rd = x.body.getReader(), n = 0; function spd() { return humansize(n / ((Date.now() + 1 - t0) / 1000)) + '/s'; } function drop(x) { if (x && x.done) return console.log('xhr-preload finished, ' + spd()); if (x && x.value && x.value.length) n += x.value.length; if (mpl.preload_url !== url || n >= 128 * 1024 * 1024) { console.log('xhr-preload aborted at ' + Math.floor(n / 1024) + ' KiB, ' + spd() + ' for ' + url); return rd.cancel(); } return rd.read().then(drop); } drop(); }); r.nopause(); r.au2.ld = 0; //owa r.au2.onloadeddata = r.au2.onloadedmetadata = r.onpreload; r.au2.preload = "auto"; r.au2.src = r.au2.rsrc = url; if (mpl.prescan_evp) { mpl.prescan_evp = null; toast.ok(7, L.mm_scank + "\n" + esc(fname)); } console.log("preloading " + fname); }; r.nopause = function () { r.cd_pause = Date.now(); }; r.onpreload = function () { r.nopause(); this.ld++; }; r.init_fau = function () { if (r.fau || !mpl.fau) return; // breaks touchbar-macs console.log('init fau'); r.fau = new Audio(SR + '/.cpr/w/deps/busy.mp3?_=' + TS); r.fau.loop = true; r.fau.play(); }; r.set_ev = function () { mp.au.onended = evau_end; mp.au.onerror = evau_error; mp.au.onprogress = pbar.drawpos; mp.au.onplaying = mpui.progress_updater; mp.au.onloadeddata = mp.au.onloadedmetadata = mp.nopause; }; } function ft2dict(tr, skip) { var th = ebi('files').tHead.rows[0].cells, rv = [], rh = [], ra = [], rt = {}; skip = skip || {}; for (var a = 1, aa = th.length; a < aa; a++) { var tv = tr.cells[a].textContent, tk = a == 1 ? 'file' : th[a].getAttribute('name').split('/').pop().toLowerCase(), vis = th[a].className.indexOf('min') === -1; if (!tv || skip[tk]) continue; (vis ? rv : rh).push(tk); ra.push(tk); rt[tk] = tv; } return [rt, rv, rh, ra]; } // toggle player widget var widget = (function () { var r = {}, widget = ebi('widget'), wtico = ebi('wtico'), nptxt = ebi('nptxt'), npirc = ebi('npirc'), m3ua = ebi('m3ua'), m3uc = ebi('m3uc'), touchmode = false, was_paused = true; r.open = function () { return r.set(true); }; r.close = function () { return r.set(false); }; r.set = function (is_open) { if (r.is_open == is_open) return false; clmod(document.documentElement, 'np_open', is_open); clmod(widget, 'open', is_open); bcfg_set('au_open', r.is_open = is_open); if (vbar) { pbar.onresize(); vbar.onresize(); } return true; }; r.toggle = function (e) { r.open() || r.close(); ev(e); return false; }; r.paused = function (paused) { if (was_paused != paused) { was_paused = paused; ebi('bplay').innerHTML = paused ? '▶' : '⏸'; } }; r.setvis = function () { widget.style.display = !has(perms, "read") || showfile.abrt ? 'none' : ''; }; wtico.onclick = function (e) { if (!touchmode) r.toggle(e); return false; }; npirc.onclick = nptxt.onclick = function (e) { ev(e); var irc = this.getAttribute('id') == 'npirc', ck = irc ? '06' : '', cv = irc ? '07' : '', m = ck + 'np: ', npk = mpl.np[1], np = mpl.np[0]; for (var a = 0; a < npk.length; a++) m += (npk[a] == 'file' ? '' : npk[a]).replace(/^\./, '') + '(' + cv + np[npk[a]] + ck + ') // '; m += '[' + cv + s2ms(mp.au.currentTime) + ck + '/' + cv + s2ms(mp.au.duration) + ck + ']'; cliptxt(m, function () { toast.ok(1, L.clipped, null, 'top'); }); }; m3ua.onclick = function (e) { ev(e); var el, files = [], sel = msel.getsel(); for (var a = 0; a < sel.length; a++) { el = ebi(sel[a].id).closest('tr'); if (clgot(el, 'au')) files.push(el); } el = QS('#files tr.play'); if (!sel.length && el) files.push(el); for (var a = 0; a < files.length; a++) { var md = ft2dict(files[a])[0], dur = md['.dur'] || '1', tag = ''; if (md.artist && md.title) tag = md.artist + ' - ' + md.title; else if (md.artist) tag = md.artist + ' - ' + md.file; else if (md.title) tag = md.title; if (dur.indexOf(':') > 0) { dur = dur.split(':'); dur = 60 * parseInt(dur[0]) + parseInt(dur[1]); } else dur = parseInt(dur); mpl.m3ut += '#EXTINF:' + dur + ',' + tag + '\n' + uricom_dec(get_evpath()) + md.file + '\n'; } toast.ok(2, files.length == 1 ? L.m3u_add1 : L.m3u_addn.format(files.length), null, 'top'); }; m3uc.onclick = function (e) { ev(e); cliptxt(mpl.m3ut, function () { toast.ok(15, L.m3u_clip, null, 'top'); }); }; r.set(sread('au_open') == 1); setTimeout(function () { clmod(widget, 'anim', 1); }, 10); return r; })(); function canvas_cfg(can) { var r = {}, b = can.getBoundingClientRect(), mul = window.devicePixelRatio || 1; r.w = b.width; r.h = b.height; can.width = r.w * mul; can.height = r.h * mul; r.can = can; r.ctx = can.getContext('2d'); r.ctx.scale(mul, mul); return r; } function glossy_grad(can, h, s, l) { var g = can.ctx.createLinearGradient(0, 0, 0, can.h), p = [0, 0.49, 0.50, 1]; for (var a = 0; a < p.length; a++) g.addColorStop(p[a], 'hsl(' + h + ',' + s[a] + '%,' + l[a] + '%)'); return g; } // buffer/position bar var pbar = (function () { var r = {}, bau = null, html_txt = 'a', lastmove = 0, mousepos = 0, t_redraw = 0, gradh = -1, grad; r.onresize = function () { if (!widget.is_open && r.buf) return; r.buf = canvas_cfg(ebi('barbuf')); r.pos = canvas_cfg(ebi('barpos')); r.buf.ctx.font = '.5em sans-serif'; r.pos.ctx.font = '.9em sans-serif'; r.pos.ctx.strokeStyle = 'rgba(24,56,0,0.5)'; r.drawbuf(); r.drawpos(); if (!r.pos.can.onmouseleave) mleave(); }; r.loadwaves = function (url) { r.wurl = url; var img = new Image(); img.onload = function () { if (r.wurl != url) return; r.wimg = img; r.onresize(); }; img.src = url; }; r.unwave = function () { r.wurl = r.wimg = null; } function mmove(e) { var adur; if (e.buttons || !mp || !mp.au || !isNum(adur = mp.au.duration)) return; var rect = r.pos.can.getBoundingClientRect(), x = e.clientX - rect.left, mul = x * 1.0 / rect.width; mousepos = adur * mul; lastmove = Date.now(); r.drawpos(); } function menter() { r.pos.can.onmousemove = mmove; r.pos.can.onmouseleave = mleave; } function mleave() { r.pos.can.onmousemove = null; r.pos.can.onmouseleave = null; r.pos.can.onmouseenter = menter; if (lastmove) { lastmove = 0; r.drawpos(); } } r.drawbuf = function () { var bc = r.buf, pc = r.pos, bctx = bc.ctx, apos, adur; if (!widget.is_open) return; bctx.clearRect(0, 0, bc.w, bc.h); if (!mp || !mp.au || !isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos) return; // not-init || unsupp-codec bau = mp.au; var sm = bc.w * 1.0 / mp.au.duration, gk = bc.h + '/' + themen, dz = themen == 'dz', dy = themen == 'dy'; if (gradh != gk) { gradh = gk; grad = glossy_grad(bc, dz ? 120 : 85, dy ? [0, 0, 0, 0] : [35, 40, 37, 35], dy ? [20, 24, 22, 20] : light ? [45, 56, 50, 45] : [42, 51, 47, 42]); } bctx.fillStyle = grad; for (var a = 0; a < mp.au.buffered.length; a++) { var x1 = sm * mp.au.buffered.start(a), x2 = sm * mp.au.buffered.end(a); bctx.fillRect(x1, 0, x2 - x1, bc.h); } if (r.wimg) { bctx.globalAlpha = 0.6; bctx.filter = light ? '' : 'invert(1)'; bctx.drawImage(r.wimg, 0, 0, bc.w, bc.h); bctx.filter = 'invert(0)'; bctx.globalAlpha = 1; } var step = sm > 1 ? 1 : sm > 0.4 ? 3 : sm > 0.05 ? 30 : 720; bctx.fillStyle = light && !dy ? 'rgba(0,64,0,0.15)' : 'rgba(204,255,128,0.15)'; for (var p = step, mins = adur / 10; p <= mins; p += step) bctx.fillRect(Math.floor(sm * p * 10), 0, 2, pc.h); step = sm > 0.15 ? 1 : sm > 0.05 ? 10 : 360; bctx.fillStyle = light && !dy ? 'rgba(0,64,0,0.5)' : 'rgba(192,255,96,0.5)'; for (var p = step, mins = adur / 60; p <= mins; p += step) bctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h); step = sm > 0.33 ? 1 : sm > 0.15 ? 5 : sm > 0.05 ? 10 : sm > 0.01 ? 60 : 720; bctx.fillStyle = dz ? '#0f0' : dy ? '#999' : light ? 'rgba(0,64,0,0.9)' : 'rgba(192,255,96,1)'; for (var p = step, mins = adur / 60; p <= mins; p += step) { bctx.fillText(p, Math.floor(sm * p * 60 + 3), pc.h / 3); } step = sm > 0.2 ? 10 : sm > 0.1 ? 30 : sm > 0.01 ? 60 : sm > 0.005 ? 720 : 1440; bctx.fillStyle = light ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)'; for (var p = step, mins = adur / 60; p <= mins; p += step) bctx.fillRect(Math.floor(sm * p * 60), 0, 2, pc.h); }; r.drawpos = function () { var bc = r.buf, pc = r.pos, pctx = pc.ctx, w = 8, apos, adur; if (t_redraw) { clearTimeout(t_redraw); t_redraw = 0; } pctx.clearRect(0, 0, pc.w, pc.h); if (!mp || !mp.au) return; // not-init if (!isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos) { if (Date.now() - mp.au.pt0 < 500) return; pctx.fillStyle = light ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)'; var m = /[?&]th=(opus|owa|caf|mp3)/.exec('' + mp.au.rsrc), txt = mp.au.ded ? L.mm_playerr.replace(':', ' ;_;') : m ? L.mm_bconv.format(m[1]) : L.mm_bload; pctx.fillText(txt, 16, pc.h / 1.5); return; // not-init || unsupp-codec } if (bau != mp.au) r.drawbuf(); if (Date.now() - lastmove < 400) { apos = mousepos; w = 0; } var sm = bc.w * 1.0 / adur, t1 = s2ms(adur), t2 = s2ms(apos), x = sm * apos; if (w && html_txt != t2) { ebi('np_pos').textContent = html_txt = t2; if (mpl.os_ctl) navigator.mediaSession.setPositionState({ 'duration': adur, 'position': apos, 'playbackRate': 1 }); } if (!widget.is_open) return; pctx.fillStyle = '#573'; pctx.fillRect((x - w / 2) - 1, 0, w + 2, pc.h); pctx.fillStyle = '#dfc'; pctx.fillRect((x - w / 2), 0, w, pc.h); pctx.lineWidth = 2.5; pctx.fillStyle = '#fff'; var m1 = pctx.measureText(t1), m1b = pctx.measureText(t1 + ":88"), m2 = pctx.measureText(t2), yt = pc.h * 0.94, xt1 = pc.w - (m1.width + 12), xt2 = x < m1.width * 1.4 ? (x + 12) : (Math.min(pc.w - m1b.width, x - 12) - m2.width); pctx.strokeText(t1, xt1 + 1, yt + 1); pctx.strokeText(t2, xt2 + 1, yt + 1); pctx.strokeText(t1, xt1, yt); pctx.strokeText(t2, xt2, yt); pctx.fillText(t1, xt1, yt); pctx.fillText(t2, xt2, yt); if (sm > 10) t_redraw = setTimeout(r.drawpos, sm > 50 ? 20 : 50); }; onresize100.add(r.onresize, true); return r; })(); // volume bar var vbar = (function () { var r = {}, gradh = -1, lastv = -1, untext = -1, can, ctx, w, h, grad1, grad2; r.onresize = function () { if (!widget.is_open && r.can) return; r.can = canvas_cfg(ebi('pvol')); can = r.can.can; ctx = r.can.ctx; ctx.font = '.7em sans-serif'; ctx.fontVariantCaps = 'small-caps'; w = r.can.w; h = r.can.h; r.draw(); } r.draw = function () { if (!mp) return; var gh = h + '' + light, dz = themen == 'dz', dy = themen == 'dy'; if (gradh != gh) { gradh = gh; grad1 = glossy_grad(r.can, dz ? 120 : 50, dy ? [0, 0, 0, 0] : light ? [50, 55, 52, 48] : [45, 52, 47, 43], dy ? [20, 24, 22, 20] : light ? [54, 60, 52, 47] : [42, 51, 47, 42]); grad2 = glossy_grad(r.can, dz ? 120 : 205, dz ? [100, 100, 100, 100] : dy ? [0, 0, 0, 0] : [10, 15, 13, 10], dz ? [10, 14, 12, 10] : dy ? [90, 90, 90, 90] : [16, 20, 18, 16]); } ctx.fillStyle = grad2; ctx.fillRect(0, 0, w, h); ctx.fillStyle = grad1; ctx.fillRect(0, 0, w * mp.vol, h); var vt = 'volume ' + Math.floor(mp.vol * 100), tw = ctx.measureText(vt).width, x = w * mp.vol - tw - 8, li = dy; if (mp.vol < 0.5) { x += tw + 16; li = !li; } ctx.fillStyle = li ? '#fff' : '#210'; ctx.fillText(vt, x, h / 3 * 2); clearTimeout(untext); untext = setTimeout(r.draw, 1000); }; onresize100.add(r.onresize, true); var rect; function mousedown(e) { rect = can.getBoundingClientRect(); mousemove(e); } function mousemove(e) { if (e.changedTouches && e.changedTouches.length > 0) { e = e.changedTouches[0]; } else if (e.buttons === 0) { can.onmousemove = null; return; } var x = e.clientX - rect.left, mul = x * 1.0 / rect.width; if (mul > 0.98) mul = 1; lastv = Date.now(); mp.setvol(mul); r.draw(); setTimeout(function () { if (IPHONE && mp.au && mul < 0.9 && mp.au.volume == 1) toast.inf(6, 'volume doesnt work because apple says no'); }, 1); } can.onmousedown = function (e) { if (e.button !== 0) return; can.onmousemove = mousemove; mousedown(e); }; can.onmouseup = function (e) { if (e.button === 0) can.onmousemove = null; }; if (TOUCH) { can.ontouchstart = mousedown; can.ontouchmove = mousemove; } return r; })(); function seek_au_mul(mul) { if (mp.au) seek_au_sec(mp.au.duration * mul); } function seek_au_rel(sec) { if (mp.au) seek_au_sec(mp.au.currentTime + sec); } function seek_au_sec(seek) { if (!mp.au) return; console.log('seek: ' + seek); if (!isNum(seek)) return; mp.nopause(); mp.au.currentTime = seek; if (mp.au.paused) mp.fade_in(); mpui.progress_updater(); } function song_skip(n, dirskip) { var tid = mp.au && mp.au.evp == get_evpath() ? mp.au.tid : null, ofs = tid ? mp.order.indexOf(tid) : -1; if (dirskip && ofs + 1 && ofs > mp.order.length - 2) { toast.inf(10, L.mm_nof); console.log("mm_nof1"); mpl.traversals = 0; return; } if (tid && !dirskip) play(ofs + n); else play(mp.order[n == -1 ? mp.order.length - 1 : 0]); } function next_song(e) { ev(e); if (QS('.dumb_loader_thing')) { treectl.ls_cb = next_song; return; } if (mp.order.length) { var dirskip = mpl.traversals; mpl.traversals = 0; return song_skip(1, dirskip); } if (mpl.traversals++ < 5) { treectl.ls_cb = next_song; return tree_neigh(1); } toast.inf(10, L.mm_nof); console.log("mm_nof2"); mpl.traversals = 0; } function last_song(e) { ev(e); if (mp.order.length) { mpl.traversals = 0; return song_skip(-1, true); } if (mpl.traversals++ < 5) { treectl.ls_cb = last_song; return tree_neigh(-1); } toast.inf(10, L.mm_nof); console.log("mm_nof2"); mpl.traversals = 0; } function prev_song(e) { ev(e); if (mp.au && !mp.au.paused && mp.au.currentTime > 3) return seek_au_sec(0); if (QS('.dumb_loader_thing')) { treectl.ls_cb = function () { song_skip(-1); }; return; } return song_skip(-1); } function dl_song() { if (!mp || !mp.au) { var o = QSA('#files a[id]'); for (var a = 0; a < o.length; a++) o[a].setAttribute('download', ''); return toast.inf(10, L.f_dls); } var url = addq(mp.au.osrc, 'cache=987&_=' + ACB); dl_file(url); } function sel_song() { var o = QS('#files tr.play'); if (!o) return; clmod(o, 'sel', 't'); msel.origin_tr(o); msel.selui(); } function playpause(e) { // must be event-chain ev(e); if (mp.au) { if (mp.au.paused) mp.fade_in(); else mp.fade_out(); mpui.progress_updater(); } else play(0, true); mpl.pp(); }; function mplay(e) { if (mp.au && !mp.au.paused) return; playpause(e); } function mpause(e) { if (mp.cd_pause > Date.now() - 100) return; if (mp.au && mp.au.paused) return; playpause(e); } // hook up the widget buttons (function () { ebi('bplay').onclick = playpause; ebi('bprev').onclick = prev_song; ebi('bnext').onclick = next_song; var bar = ebi('barpos'); bar.onclick = function (e) { if (!mp.au) { play(0, true); return mp.fade_in(); } var rect = pbar.buf.can.getBoundingClientRect(), x = e.clientX - rect.left; seek_au_mul(x * 1.0 / rect.width); }; if (!TOUCH) { bar.onwheel = function (e) { var dist = Math.sign(e.deltaY) * 10; if (Math.abs(e.deltaY) < 30 && !e.deltaMode) dist = e.deltaY; if (!dist || !mp.au) return true; seek_au_rel(dist); ev(e); }; ebi('pvol').onwheel = function (e) { var dist = Math.sign(e.deltaY) * 10; if (Math.abs(e.deltaY) < 30 && !e.deltaMode) dist = e.deltaY; if (!dist || !mp.au) return true; dist *= -1; mp.setvol(Math.round((mp.vol + dist / 500) * 100) / 100 ); vbar.draw(); ev(e); }; } })(); // periodic tasks var mpui = (function () { var r = {}, nth = 0, preloaded = null, fpreloaded = null; r.progress_updater = function () { //console.trace(); timer.add(updater_impl, true); }; function repreload() { preloaded = fpreloaded = null; } function updater_impl() { if (!mp.au) { widget.paused(true); timer.rm(updater_impl); return; } var paint = !MOBILE || document.hasFocus(); var pos = mp.au.currentTime; if (!isNum(pos)) pos = 0; // indicate playback state in ui widget.paused(mp.au.paused); if (paint && ++nth > 69) { // android-chrome breaks aspect ratio with unannounced viewport changes nth = 0; if (MOBILE) { nth = 1; pbar.onresize(); vbar.onresize(); } } else if (paint) { // draw current position in song if (!mp.au.paused) pbar.drawpos(); // occasionally draw buffered regions if (nth % 5 == 0) pbar.drawbuf(); } // preload next song if (!mpl.one && mpl.preload && preloaded != mp.au.rsrc) { var len = mp.au.duration, rem = pos > 1 ? len - pos : 999, full = null; if (rem < 7 || (!mpl.fullpre && (rem < 40 || (rem < 90 && pos > 10)))) { preloaded = fpreloaded = mp.au.rsrc; full = false; } else if (rem < 60 && mpl.fullpre && fpreloaded != mp.au.rsrc) { fpreloaded = mp.au.rsrc; full = true; } if (full !== null) try { var oi = mp.order.indexOf(mp.au.tid) + 1, evp = get_evpath(); if (oi >= mp.order.length && ( mpl.one || mpl.pb_mode != 'next' || mp.au.evp != evp || ebi('unsearch')) ) oi = 0; if (oi >= mp.order.length) { if (!mpl.prescan) throw "prescan disabled"; if (mpl.prescan_evp == evp) throw "evp match"; if (treectl.trunc) return treectl.showmore(99999, repreload); if (mpl.traversals++ > 4) { mpl.prescan_evp = null; toast.inf(10, L.mm_nof); throw L.mm_nof; } mpl.prescan_evp = evp; toast.inf(10, L.mm_prescan); treectl.ls_cb = repreload; tree_neigh(1); } else mp.preload(mp.tracks[mp.order[oi]], full); } catch (ex) { console.log("preload failed", ex); } } if (mp.au.paused) timer.rm(updater_impl); } return r; })(); // event from play button next to a file in the list function ev_play(e) { ev(e); var fade = !mp.au || mp.au.paused; play(this.getAttribute('id').slice(1), true); if (fade) mp.fade_in(); return false; } var actx = null; function start_actx() { // bonus: speedhack for unfocused file hashing (removes 1sec delay on subtle.digest resolves) if (!actx) { if (!ACtx) return; actx = new ACtx(); console.log('actx created'); } try { if (actx.state == 'suspended') { actx.resume(); setTimeout(function () { console.log('actx is ' + actx.state); }, 500); } } catch (ex) { console.log('actx start failed; ' + ex); } } var afilt = (function () { var r = { "eqen": false, "drcen": false, "ssen": false, "bands": [31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000], "gains": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4], "drcv": [-24, 30, 12, 0.01, 0.25], "drch": ['tresh', 'knee', 'ratio', 'atk', 'rls'], "drck": ['threshold', 'knee', 'ratio', 'attack', 'release'], "sscl": [L.mt_ssvt, L.mt_ssts, L.mt_sste, L.mt_sssm], "sscv": [1, 5, 5, 5.0], "drcn": null, "filters": [], "filterskip": [], "plugs": [], "amp": 0, "chw": 1, "last_au": null, "acst": {} }; function setvis(vis) { ebi('audio_eq').parentNode.style.display = ebi('audio_drc').parentNode.style.display = ebi('audio_ss').parentNode.style.display = (vis ? '' : 'none'); } setvis(ACtx); r.init = function () { start_actx(); if (r.cfg) return; setvis(actx); // some browsers have insane high-frequency boost // (or rather the actual problem is Q but close enough) r.cali = (function () { try { var fi = actx.createBiquadFilter(), freqs = new Float32Array(1), mag = new Float32Array(1), phase = new Float32Array(1); freqs[0] = 14000; fi.type = 'peaking'; fi.frequency.value = 18000; fi.Q.value = 0.8; fi.gain.value = 1; fi.getFrequencyResponse(freqs, mag, phase); return mag[0]; // 1.0407 good, 1.0563 bad } catch (ex) { return 0; } })(); console.log('eq cali: ' + r.cali); var e1 = r.cali < 1.05; r.cfg = [ // hz, q, g [31.25 * 0.88, 0, 1.4], // shelf [31.25 * 1.04, 0.7, 0.96], // peak [62.5, 0.7, 1], [125, 0.8, 1], [250, 0.9, 1.03], [500, 0.9, 1.1], [1000, 0.9, 1.1], [2000, 0.9, 1.105], [4000, 0.88, 1.05], [8000 * 1.006, 0.73, e1 ? 1.24 : 1.2], [16000 * 0.89, 0.7, e1 ? 1.26 : 1.2], // peak [16000 * 1.13, 0.82, e1 ? 1.09 : 0.75], // peak [16000 * 1.205, 0, e1 ? 1.9 : 1.85] // shelf ]; }; try { r.amp = fcfg_get('au_eq_amp', r.amp); r.chw = fcfg_get('au_eq_chw', r.chw); var gains = jread('au_eq_gain', r.gains); if (r.gains.length == gains.length) r.gains = gains; r.drcv = jread('au_drcv', r.drcv); r.sscv = jread('au_sscv', r.sscv); mpss.load(); } catch (ex) { } r.draw = function () { jwrite('au_eq_gain', r.gains); swrite('au_eq_amp', r.amp); swrite('au_eq_chw', r.chw); var txt = QSA('input.eq_gain'); for (var a = 0; a < r.bands.length; a++) txt[a].value = r.gains[a]; QS('input.eq_gain[band="amp"]').value = r.amp; QS('input.eq_gain[band="chw"]').value = r.chw; }; r.stop = function () { mpss.stop(); if (r.filters.length) for (var a = 0; a < r.filters.length; a++) r.filters[a].disconnect(); r.filters = []; r.filterskip = []; for (var a = 0; a < r.plugs.length; a++) r.plugs[a].unload(); if (!mp) return; if (mp.acs) mp.acs.disconnect(); mp.acs = mpo.acs = null; }; r.apply = function (v, au) { r.init(); r.draw(); if (!actx) { bcfg_set('au_eq', r.eqen = false); bcfg_set('au_drc', r.drcen = false); } else if (v === true && (r.drcen || r.ssen) && !r.eqen) bcfg_set('au_eq', r.eqen = true); else if (v === false && !r.eqen) { bcfg_set('au_drc', r.drcen = false); bcfg_set('au_ss', r.ssen = false); } r.drcn = null; var plug = false; for (var a = 0; a < r.plugs.length; a++) if (r.plugs[a].en) plug = true; au = au || (mp && mp.au); if (!actx || !au || (!r.eqen && !plug && !mp.acs)) return; r.stop(); au.id = au.id || Date.now(); mp.acs = r.acst[au.id] = r.acst[au.id] || actx.createMediaElementSource(au); if (r.ssen) add_ss(); if (r.eqen) add_eq(); for (var a = 0; a < r.plugs.length; a++) if (r.plugs[a].en) r.plugs[a].load(); for (var a = 0; a < r.filters.length; a++) if (!has(r.filterskip, a)) r.filters[a].connect(a ? r.filters[a - 1] : actx.destination); mp.acs.connect(r.filters.length ? r.filters[r.filters.length - 1] : actx.destination); if (!au.paused) mpss.go(); } function add_ss() { r.filters.push(afilt.ssg = actx.createGain()); r.filters.push(afilt.ssa = actx.createAnalyser()); afilt.ssa.fftSize = 256; } function add_eq() { var min, max; min = max = r.gains[0]; for (var a = 1; a < r.gains.length; a++) { min = Math.min(min, r.gains[a]); max = Math.max(max, r.gains[a]); } var gains = []; for (var a = 0; a < r.gains.length; a++) gains.push(r.gains[a] - max); var t = gains[gains.length - 1]; gains.push(t); gains.push(t); gains.unshift(gains[0]); for (var a = 0; a < r.cfg.length && min != max; a++) { var fi = actx.createBiquadFilter(), c = r.cfg[a]; fi.frequency.value = c[0]; fi.gain.value = c[2] * gains[a]; fi.Q.value = c[1]; fi.type = a == 0 ? 'lowshelf' : a == r.cfg.length - 1 ? 'highshelf' : 'peaking'; r.filters.push(fi); } // pregain, keep first in chain fi = actx.createGain(); fi.gain.value = r.amp + 0.94; // +.137 dB measured; now -.25 dB and almost bitperfect r.filters.push(fi); // wait nevermind, drc goes first timer.rm(showdrc); if (r.drcen) { fi = r.drcn = actx.createDynamicsCompressor(); for (var a = 0; a < r.drcv.length; a++) fi[r.drck[a]].value = r.drcv[a]; if (r.drcv[3] < 0.02) { // avoid static at decode start fi.attack.value = 0.02; setTimeout(function () { try { fi.attack.value = r.drcv[3]; } catch (ex) { } }, 200); } r.filters.push(fi); timer.add(showdrc); } if (Math.round(r.chw * 25) != 25) { var split = actx.createChannelSplitter(2), merge = actx.createChannelMerger(2), lg1 = actx.createGain(), lg2 = actx.createGain(), rg1 = actx.createGain(), rg2 = actx.createGain(), vg1 = 1 - (1 - r.chw) / 2, vg2 = 1 - vg1; console.log('chw', vg1, vg2); merge.connect(r.filters[r.filters.length - 1]); lg1.gain.value = rg2.gain.value = vg1; lg2.gain.value = rg1.gain.value = vg2; lg1.connect(merge, 0, 0); rg1.connect(merge, 0, 0); lg2.connect(merge, 0, 1); rg2.connect(merge, 0, 1); split.connect(lg1, 0); split.connect(lg2, 0); split.connect(rg1, 1); split.connect(rg2, 1); r.filterskip.push(r.filters.length); r.filters.push(split); mp.acs.channelCountMode = 'explicit'; } } function eq_step(e) { ev(e); var sb = this.getAttribute('band'), band = parseInt(sb), step = parseFloat(this.getAttribute('step')); if (sb == 'amp') r.amp = Math.round((r.amp + step * 0.2) * 100) / 100; else if (sb == 'chw') r.chw = Math.round((r.chw + step * 0.2) * 100) / 100; else r.gains[band] += step; r.apply(); } function adj_band(that, step) { var err = false; try { var sb = that.getAttribute('band'), band = parseInt(sb), vs = that.value, v = parseFloat(vs); if (!isNum(v) || v + '' != vs) throw new Error('inval band'); if (sb == 'amp') r.amp = Math.round((v + step * 0.2) * 100) / 100; else if (sb == 'chw') r.chw = Math.round((v + step * 0.2) * 100) / 100; else r.gains[band] = v + step; r.apply(); } catch (ex) { err = true; } clmod(that, 'err', err); } function adj_drc() { var err = false; try { var n = this.getAttribute('k'), ov = r.drcv[n], vs = this.value, v = parseFloat(vs); if (!isNum(v) || v + '' != vs) throw new Error('inval v'); if (v == ov) return; r.drcv[n] = v; jwrite('au_drcv', r.drcv); if (r.drcn) r.drcn[r.drck[n]].value = v; } catch (ex) { err = true; } clmod(this, 'err', err); } function adj_ss() { var err = false; try { var n = this.getAttribute('k'), ov = r.sscv[n], vs = this.value, v = parseFloat(vs); if (!isNum(v) || v + '' != vs) throw new Error('inval v'); if (v == ov) return; r.sscv[n] = v; jwrite('au_sscv', r.sscv); mpss.load(); } catch (ex) { err = true; } clmod(this, 'err', err); } function eq_mod(e) { ev(e); adj_band(this, 0); } function eq_keydown(e) { var step = e.key == 'ArrowUp' ? 0.25 : e.key == 'ArrowDown' ? -0.25 : 0; if (step != 0) adj_band(this, step); } function showdrc() { if (!r.drcn) return timer.rm(showdrc); ebi('h_drc').textContent = f2f(r.drcn.reduction, 1); } var html = [''], h2 = [], h3 = [], h4 = []; var vs = []; for (var a = 0; a < r.bands.length; a++) { var hz = r.bands[a]; if (hz >= 1000) hz = (hz / 1000) + 'k'; hz = (hz + '').split('.')[0]; vs.push([a, hz, r.gains[a]]); } vs.push(["amp", "boost", r.amp]); vs.push(["chw", "width", r.chw]); for (var a = 0; a < vs.length; a++) { var b = vs[a][0]; html.push(''); h2.push(''); h4.push(''); h3.push(''); } html = html.join('\n') + ''; html += h2.join('\n') + ''; html += h3.join('\n') + ''; html += h4.join('\n') + '
          ', '' + L.enable + '+' + vs[a][1] + '
          '; ebi('audio_eq').innerHTML = html; h2 = []; html = ['
          ']; for (var a = 0; a < r.drch.length; a++) { html.push(''); h2.push(''); } html = html.join('\n') + ''; html += h2.join('\n') + '
          ', '' + L.enable + '' + r.drch[a] + '
          '; ebi('audio_drc').innerHTML = html; h2 = []; html = ['
          ']; for (var a = 0; a < r.sscl.length; a++) { html.push(''); } html = html.join('\n') + ''; html += h2.join('\n') + '
          ', '' + L.enable + '
          '; ebi('audio_ss').innerHTML = html; var stp = QSA('a.eq_step'); for (var a = 0, aa = stp.length; a < aa; a++) stp[a].onclick = eq_step; var txt = QSA('input.eq_gain'); for (var a = 0; a < txt.length; a++) { txt[a].oninput = eq_mod; txt[a].onkeydown = eq_keydown; } txt = QSA('input.drc_v'); for (var a = 0; a < txt.length; a++) txt[a].oninput = txt[a].onkeydown = adj_drc; txt = QSA('input.ssconf_v'); for (var a = 0; a < txt.length; a++) txt[a].oninput = txt[a].onkeydown = adj_ss; bcfg_bind(r, 'eqen', 'au_eq', false, r.apply); bcfg_bind(r, 'drcen', 'au_drc', false, r.apply); bcfg_bind(r, 'ssen', 'au_ss', false, r.apply); r.draw(); return r; })(); // plays the tid'th audio file on the page function play(tid, is_ev, seek) { clearTimeout(mpl.t_eplay); if (mp.order.length == 0) return console.log('no audio found wait what'); if (crashed) return; mpl.preload_url = null; mp.nopause(); mp.stopfade(true); var tn = tid; if ((tn + '').indexOf('f-') === 0) { tn = mp.order.indexOf(tn); if (tn < 0) return toast.warn(10, L.mm_hnf); } if (tn >= mp.order.length) { if (treectl.trunc) return treectl.showmore(99999, next_song); if (mpl.pb_mode == 'loop' || ebi('unsearch')) { tn = 0; } else if (mpl.pb_mode == 'next') { treectl.ls_cb = next_song; return tree_neigh(1); } else return; } if (tn < 0) { if (mpl.pb_mode == 'loop') { tn = mp.order.length - 1; } else if (mpl.pb_mode == 'next') { treectl.ls_cb = last_song; return tree_neigh(-1); } else return; } tid = mp.order[tn]; if (mp.au) { mp.au.pause(); var el = ebi('a' + mp.au.tid); if (el) clmod(el, 'act'); } else { mp.au = new Audio(); mp.au2 = new Audio(); mp.set_ev(); widget.open(); } mp.init_fau(); var url = addq(mpl.acode(mp.tracks[tid]), 'cache=987&_=' + ACB); if (mp.au.rsrc == url) mp.au.currentTime = 0; else if (mp.au2.rsrc == url) { var t = mp.au; mp.au = mp.au2; mp.au2 = t; t.onerror = t.onprogress = t.onended = t.loop = null; t.ld = 0; //owa mp.set_ev(); t = mp.au.currentTime; if (isNum(t) && t > 0.1) mp.au.currentTime = 0; } else { console.log('get ' + url.split('/').pop()); mp.au.src = mp.au.rsrc = url; } mp.au.osrc = mp.tracks[tid]; afilt.apply(); setTimeout(function () { mpl.unbuffer(url); }, 500); mp.au.ded = 0; mp.au.tid = tid; mp.au.pt0 = Date.now(); mp.au.evp = get_evpath(); mp.au.volume = mp.expvol(mp.vol); var trs = QSA('#files tr.play'); for (var a = 0, aa = trs.length; a < aa; a++) clmod(trs[a], 'play'); var oid = 'a' + tid, t_a = ebi(oid), t_tr = t_a.closest('tr'); clmod(t_a, 'act', 1); clmod(t_tr, 'play', 1); clmod(ebi('wtoggle'), 'np', mpl.clip); clmod(ebi('wtoggle'), 'm3u', mpl.m3uen); if (thegrid) thegrid.loadsel(); if (mpl.follow) scroll2playing(); try { mp.nopause(); mp.au.loop = mpl.loop && !mpl.one; if (mpl.aplay || is_ev !== -1) mp.au.play(); if (mp.au.paused) autoplay_blocked(seek); else if (seek) { seek_au_sec(seek); } if (!seek && !ebi('unsearch')) { t_a.setAttribute('id', 'thx_js'); if (mpl.aplay) sethash(oid + getsort()); t_a.setAttribute('id', oid); } mpl.np = ft2dict(t_tr, { 'up_ip': 1 }); pbar.unwave(); if (mpl.waves) pbar.loadwaves(url.replace(/\bth=(opus|mp3)&/, '') + '&th=p'); mpss.go(); mpui.progress_updater(); pbar.onresize(); vbar.onresize(); mpl.announce(); return true; } catch (ex) { toast.err(0, esc(L.mm_playerr + basenames(ex))); } clmod(t_a, 'act'); mpl.t_eplay = setTimeout(next_song, 5000); } function scroll2playing() { try { QS((!thegrid || !thegrid.en) ? 'tr.play' : '#ggrid a.play').scrollIntoView(); } catch (ex) { } } function evau_end(e) { if (mpl.one) return; if (!mpl.loop) return next_song(e); ev(e); mp.au.currentTime = 0; mp.au.play(); } // event from the audio object if something breaks function evau_error(e) { var err = '', eplaya = (e && e.target) || (window.event && window.event.srcElement); eplaya.ded = 1; switch (eplaya.error.code) { case eplaya.error.MEDIA_ERR_ABORTED: err = L.mm_eabrt; break; case eplaya.error.MEDIA_ERR_NETWORK: if (IPHONE && eplaya.ld === 1 && mpl.ac2 == 'owa' && !eplaya.paused && !eplaya.currentTime) { eplaya.ded = 0; if (!mpl.owaw) { mpl.owaw = 1; console.log('ignored iOS bug; spurious error sent in parallel with preloaded songs starting to play just fine'); } return; } err = L.mm_enet; break; case eplaya.error.MEDIA_ERR_DECODE: err = L.mm_edec; break; case eplaya.error.MEDIA_ERR_SRC_NOT_SUPPORTED: err = L.mm_esupp; if (/\.(aac|m4[abr])(\?|$)/i.exec(eplaya.rsrc) && !mpl.ac_aac) { try { ebi('ac_aac').click(); QS('a.play.act').click(); toast.warn(10, L.mm_opusen); return; } catch (ex) { } } break; default: err = L.mm_eunk; break; } var em = '' + eplaya.error.message, mfile = '\n\nFile: «' + uricom_dec(eplaya.src.split('/').pop()) + '»', e500 = L.mm_e500, e415 = L.mm_e415, e404 = L.mm_e404, e403 = L.mm_e403; if (em) err += '\n\n' + em; if (em.startsWith('403: ')) err = e403; if (em.startsWith('404: ')) err = e404; if (em.startsWith('415: ')) err = e415; if (em.startsWith('500: ')) err = e500; toast.warn(15, esc(basenames(err + mfile))); console.log(basenames(err + mfile)); if (em.startsWith('MEDIA_ELEMENT_ERROR:')) { // chromish for 40x var xhr = new XHR(); xhr.open('HEAD', eplaya.src, true); xhr.onload = xhr.onerror = function () { if (this.status < 400) return; err = this.status == 403 ? e403 : this.status == 404 ? e404 : this.status == 415 ? e415 : this.status == 500 ? e500 : L.mm_e5xx + this.status; toast.warn(15, esc(basenames(err + mfile))); }; xhr.send(); return; } mpl.t_eplay = setTimeout(next_song, 15000); } // show ui to manually start playback of a linked song function autoplay_blocked(seek) { var tid = mp.au.tid, fn = mp.tracks[tid].split(/\//).pop(); fn = uricom_dec(fn.replace(/\+/g, ' ').split('?')[0]); modal.confirm('
          ' + L.mm_hashplay + '
          \n«' + esc(fn) + '»', function () { // chrome 91 may permanently taint on a failed play() // depending on win10 settings or something? idk mp.au = mpo.au = null; play(tid, true, seek); mp.fade_in(); }, function () { sethash(''); clmod(QS('#files tr.play'), 'play'); return reload_mp(); }); } function scan_hash(v) { if (!v) return null; var m = /^#([ag])(f-[0-9a-f]{8,16})(&.+)?/.exec(v + ''); if (!m) return null; var mtype = m[1], id = m[2], ts = null; if (m.length > 3) { var tm = /^&[Tt=0]*([0-9]+[Mm:])?0*([0-9\.]+)[Ss]?$/.exec(m[3]); if (tm) { ts = parseInt(tm[1] || 0) * 60 + parseFloat(tm[2] || 0); } tm = /^&[Tt=0]*([0-9\.]+)-([0-9\.]+)$/.exec(m[3]); if (tm) { ts = '' + tm[1] + '-' + tm[2]; } } return [mtype, id, ts]; } function eval_hash() { if (!window.hotkeys_attached) { window.hotkeys_attached = true; document.onkeydown = ahotkeys; window.onpopstate = treectl.onpopfun; } if (hash0 && window.og_fn) { var all = msel.getall(), mi; for (var a = 0; a < all.length; a++) if (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) { mi = all[a]; break; } var ch = !mi ? '' : img_re.exec(og_fn) ? 'g' : ebi('a' + mi.id) ? 'a' : ''; hash0 = ch ? ('#' + ch + mi.id) : ''; } var v = hash0; hash0 = null; if (!v) return; var media = scan_hash(v); if (media) { var mtype = media[0], id = media[1], ts = media[2]; if (mtype == 'a') { if (!ts) return play(id, -1); return play(id, -1, ts); } if (mtype == 'g') { if (!thegrid.en) ebi('griden').click(); var t = setInterval(function () { if (!thegrid.bbox) return; clearInterval(t); baguetteBox.urltime(ts); var im = QS('#ggrid a[ref="' + id + '"]'); if (!im) return toast.warn(10, L.im_hnf); if (thegrid.sel) setTimeout(function () { thegrid.sel = true; }, 1); thegrid.sel = false; im.click(); im.scrollIntoView(); }, 50); } } if (v.startsWith('#q=')) { goto('search'); var i = ebi('q_raw'); i.value = uricom_dec(v.slice(3)); return i.onkeydown({ 'key': 'Enter' }); } if (v.startsWith('#v=')) { goto(v.slice(3)); return; } if (v.startsWith("#m3u=")) { load_m3u(v.slice(5)); return; } } (function () { var props = {}; // a11y jump-to-content for (var a = 0; a < 2; a++) (function (a) { var d = mknod('a'); d.setAttribute('href', '#'); d.setAttribute('class', 'ayjump'); d.innerHTML = a ? L.ay_path : L.ay_files; document.body.insertBefore(d, ebi('ops')); d.onclick = function (e) { ev(e); if (a) d = QS(treectl.hidden ? '#path a:nth-last-child(2)' : '#treeul a.hl'); else d = QS(thegrid.en ? '#ggrid a' : '#files tbody tr[tabindex]'); if (d) d.focus(); }; })(a); // account-info label var d = mknod('div', 'acc_info'); document.body.insertBefore(d, ebi('ops')); // folder nav ebi('goh').parentElement.appendChild(mknod('span', null, '= 0; a--) { var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2]; if (!name) continue; name = name.toLowerCase(); if (name == 'ts') typ = 'int'; if (name.indexOf('tags/') === 0) { name = name.slice(5); for (var b = 0, bb = nodes.length; b < bb; b++) nodes[b]._sv = nodes[b].tags[name]; } else { for (var b = 0, bb = nodes.length; b < bb; b++) { var v = nodes[b][name]; if ((v + '').indexOf('')[1]; else if (name == "href" && v) v = uricom_dec(v); nodes[b]._sv = v } } var onodes = nodes.map(function (x) { return x; }); nodes.sort(function (n1, n2) { var v1 = n1._sv, v2 = n2._sv; if (v1 === undefined) { if (v2 === undefined) { return onodes.indexOf(n1) - onodes.indexOf(n2); } return -1 * rev; } if (v2 === undefined) return 1 * rev; var ret = rev * (typ == 'int' ? (v1 - v2) : ENATSORT ? NATSORT.compare(v1, v2) : v1.localeCompare(v2)); if (ret === 0) ret = onodes.indexOf(n1) - onodes.indexOf(n2); return ret; }); } for (var b = 0, bb = nodes.length; b < bb; b++) { delete nodes[b]._sv; if (is_srch) delete nodes[b].ext; } if (dir1st) { var r1 = [], r2 = []; for (var b = 0, bb = nodes.length; b < bb; b++) (nodes[b].href.split('?')[0].slice(-1) == '/' ? r1 : r2).push(nodes[b]); nodes = r1.concat(r2); } } catch (ex) { console.log("failed to apply sort config: " + ex); console.log("resetting fsort " + sread('fsort')); sdrop('fsort'); } return nodes; } function fmt_ren(re, md, fmt) { var ptr = 0; function dive(stop_ch) { var ret = '', ng = 0; while (ptr < fmt.length) { var dbg = fmt.slice(ptr), ch = fmt[ptr++]; if (ch == '\\') { ret += fmt[ptr++]; continue; } if (ch == ')' || ch == ']' || ch == stop_ch) return [ng, ret]; if (ch == '[') { var r2 = dive(); if (r2[0] == 0) ret += r2[1]; } else if (ch == '(') { var end = fmt.indexOf(')', ptr); if (end < 0) throw 'the ( was never closed: ' + fmt.slice(0, ptr); var arg = fmt.slice(ptr, end), v = null; ptr = end + 1; if (arg != parseInt(arg)) v = md[arg]; else { arg = parseInt(arg); if (arg >= re.length) throw 'matching group ' + arg + ' exceeds ' + (re.length - 0); v = re[arg]; } if (v !== null && v !== undefined) ret += v; else ng++; } else if (ch == '$') { ch = fmt[ptr++]; var end = fmt.indexOf('(', ptr); if (end < 0) throw 'no function name after the $ here: ' + fmt.slice(0, ptr); var fun = fmt.slice(ptr - 1, end); ptr = end + 1; if (fun == "lpad") { var str = dive(',')[1]; var len = dive(',')[1]; var chr = dive()[1]; if (!len || !chr) throw 'invalid arguments to ' + fun; if (!str.length) ng += 1; while (str.length < len) str = chr + str; ret += str; } else if (fun == "rpad") { var str = dive(',')[1]; var len = dive(',')[1]; var chr = dive()[1]; if (!len || !chr) throw 'invalid arguments to ' + fun; if (!str.length) ng += 1; while (str.length < len) str += chr; ret += str; } else throw 'function not implemented: "' + fun + '"'; } else ret += ch; } return [ng, ret]; } try { return [true, dive()[1]]; } catch (ex) { return [false, ex]; } } function enre_rw_edit() { window.re_rw_edit = new RegExp('\.(' + rw_edit.replace(/,/g, '|') + ')$', 'i'); } enre_rw_edit(); function fs_abrt() { toast.inf(30, L.fp_abrt); fileman.sn++; fileman.f.length = 0; var xhr = new XHR(); xhr.open('POST', '/?fs_abrt=' + abrt_key, true); xhr.send(); } var fileman = (function () { var bren = ebi('fren'), bdel = ebi('fdel'), bcut = ebi('fcut'), bcpy = ebi('fcpy'), bpst = ebi('fpst'), bshr = ebi('fshr'), t_paste, r = {}; r.f = []; r.sn = 1; r.clip = null; try { r.bus = new BroadcastChannel("fileman_bus"); } catch (ex) { } r.render = function () { if (r.clip === null) { r.clip = jread('fman_clip', []).slice(1); r.ccp = r.clip.length && r.clip[0] == '//c'; if (r.ccp) r.clip.shift(); } var sel = msel.getsel(), nsel = sel.length, enren = nsel, endel = nsel, encut = nsel, encpy = nsel, enpst = r.clip && r.clip.length, hren = !(have_mv && has(perms, 'write') && has(perms, 'move')), hdel = !(have_del && has(perms, 'delete')), hcut = !(have_mv && has(perms, 'move')), hpst = !(have_mv && has(perms, 'write')), hshr = !can_shr || !get_evpath().indexOf(have_shr); if (!(enren || endel || encut || enpst)) hren = hdel = hcut = hpst = true; clmod(bren, 'en', enren); clmod(bdel, 'en', endel); clmod(bcut, 'en', encut); clmod(bcpy, 'en', encpy); clmod(bpst, 'en', enpst); clmod(bshr, 'en', 1); clmod(bren, 'hide', hren); clmod(bdel, 'hide', hdel); clmod(bcut, 'hide', hcut); clmod(bpst, 'hide', hpst); clmod(bshr, 'hide', hshr); clmod(ebi('wfm'), 'act', QS('#wfm a.en:not(.hide)')); clmod(ebi('wtoggle'), 'm3u', mpl.m3uen && (nsel || (mp && mp.au))); var wfs = ebi('wfs'), h = ''; try { wfs.innerHTML = h = r.fsi(sel); } catch (ex) { } clmod(wfs, 'act', h); bpst.setAttribute('tt', L.ft_paste.format(r.clip.length)); bshr.setAttribute('tt', nsel ? L.fs_ss : L.fs_sc); }; r.fsi = function (sel) { if (!sel.length) return ''; var lf = treectl.lsc.files, nf = 0, sz = 0, dur = 0, ntab = new Set(); for (var a = 0; a < sel.length; a++) ntab.add(sel[a].vp.split('/').pop()); for (var a = 0; a < lf.length; a++) { if (!ntab.has(lf[a].href.split('?')[0])) continue; var f = lf[a]; nf++; sz += f.sz; if (f.tags && f.tags['.dur']) dur += f.tags['.dur'] } if (!nf) return ''; var ret = '{0}
          {1}F'.format(humansize(sz), nf); if (dur) ret += ' ' + s2ms(dur); return ret; }; r.share = function (e) { ev(e); var vp = uricom_dec(get_evpath()), sel = msel.getsel(), fns = []; for (var a = 0; a < sel.length; a++) fns.push(uricom_dec(noq_href(ebi(sel[a].id)))); if (fns.length == 1 && fns[0].endsWith('/')) vp = fns.pop(); for (var a = 0; a < fns.length; a++) if (fns[a].endsWith('/')) return toast.err(10, L.fs_just1d); var shui = ebi('shui'); if (!shui) { shui = mknod('div', 'shui'); document.body.appendChild(shui); } shui.style.display = 'block'; var html = [ '
          ', '
          ', '', '', '', '', '', ''; modal.confirm(txt + L.fs_ok, function() { cliptxt(surl, function () { toast.ok(2, L.clipped); }); }, null); } sh_apply.onclick = function () { if (!sh_k.value) sh_rand.click(); var plist = []; for (var a = 0; a < pbtns.length; a++) if (clgot(pbtns[a], 'on')) plist.push(pbtns[a].textContent); shui.style.display = 'none'; toast.inf(30, L.fs_w8); var body = { "k": sh_k.value, "vp": fns.length ? fns : [sh_vp.value], "pw": sh_pw.value, "exp": exm.value, "perms": plist, }; var xhr = new XHR(); xhr.open('POST', SR + '/?share', true); xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.onload = xhr.onerror = shr_cb; xhr.send(JSON.stringify(body)); }; setTimeout(sh_pw.focus.bind(sh_pw), 1); }; r.rename = function (e) { ev(e); var sel = msel.getsel(), all = msel.all; if (!sel.length) return toast.err(3, L.fr_emore); if (clgot(bren, 'hide')) return toast.err(3, L.fr_eperm); var f = [], sn = ++r.sn, base = vsplit(sel[0].vp)[0], s2d = {}, mkeys; r.f = f; r.n_s = 1; r.n_d = 1; for (var a = 0; a < sel.length; a++) { s2d[a] = all.indexOf(sel[a]); var vp = sel[a].vp; if (vp.endsWith('/')) vp = vp.slice(0, -1); var vsp = vsplit(vp); if (base != vsp[0]) return toast.err(0, esc('bug:\n' + base + '\n' + vsp[0])); var vars = ft2dict(ebi(sel[a].id).closest('tr')); mkeys = [".n.d", ".n.s"].concat(vars[1], vars[2]); var md = vars[0]; md[".n.s"] = md[".n.d"] = 0; for (var k in md) { if (!md.hasOwnProperty(k)) continue; md[k] = (md[k] + '').replace(/[\/\\]/g, '-'); if (k.startsWith('.')) md[k.slice(1)] = md[k]; } md.t = md.ext; md.date = md.ts; md.size = md.sz; f.push({ "src": vp, "ofn": uricom_dec(vsp[1]), "md": vars[0], "ok": true }); } var rui = ebi('rui'); if (!rui) { rui = mknod('div', 'rui'); document.body.appendChild(rui); } var html = sel.length > 1 ? ['
          ', '', '', '', '
          ' + L.fs_name + '
          ' + L.fs_src + '
          ' + L.fs_pwd + '
          ' + L.fs_exp + '', ' ' + L.fs_tmin + ' / ', ' ' + L.fs_thrs + ' / ', ' ' + L.fs_tdays + ' / ', '', '
          perms', ]; for (var a = 0; a < perms.length; a++) if (!has(['admin', 'move', 'delete'], perms[a])) html.push('' + perms[a] + ''); if (has(perms, 'write')) html.push('write-only'); html.push('
          ', '', '', '', '', '', '', '
          regex
          format
          preset', '
          num0', 'n.d=  ', 'n.s=', '
          ' ]); var cheap = f.length > 500, t_rst = L.frt_rst.split('>').pop(); if (sel.length == 1) html.push( '
          \n' + '\n' + ''); else { html.push( '
          old:
          new:
          ' + ''); for (var a = 0; a < f.length; a++) html.push( '' : '' + '') + '' + ''); } html.push('
          ' + L.fr_lnew + '' + L.fr_lold + '
          ' + (cheap ? '
          '); if (sel.length == 1) { html.push('

          ' + L.fr_tags + '

          '); for (var a = 0; a < mkeys.length; a++) html.push(''); html.push('
          ' + esc(mkeys[a]) + '
          '); } rui.innerHTML = html.join('\n'); for (var a = 0; a < f.length; a++) { f[a].iold = ebi('rn_old_' + a); f[a].inew = ebi('rn_new_' + a); f[a].inew.value = f[a].iold.value = f[a].ofn; if (!cheap) (function (a) { f[a].inew.onkeydown = function (e) { rn_ok(a, true); var kc = (e.key || e.code) + ''; if (kc.endsWith('Enter')) return rn_apply(); }; ebi('rn_dec_' + a).onclick = function (e) { ev(e); f[a].inew.value = uricom_dec(f[a].inew.value); }; ebi('rn_reset_' + a).onclick = function (e) { ev(e); rn_reset(a); }; })(a); } rn_reset(0); tt.att(rui); function sadv() { ebi('rn_vadv').style.display = ebi('rn_case').style.display = r.adv ? '' : 'none'; } bcfg_bind(r, 'adv', 'rn_adv', false, sadv); bcfg_bind(r, 'cs', 'rn_case', false); bcfg_bind(r, 'win', 'rn_win', true); bcfg_bind(r, 'slash', 'rn_slash', true); sadv(); function rn_ok(n, ok) { f[n].ok = ok; clmod(f[n].inew.closest('tr'), 'err', !ok); } function rn_reset(n) { f[n].inew.value = f[n].iold.value = f[n].ofn; f[n].inew.focus(); f[n].inew.setSelectionRange(0, f[n].inew.value.lastIndexOf('.'), "forward"); } function rn_cancel(e) { ev(e); rui.parentNode.removeChild(rui); } ebi('rn_cancel').onclick = rn_cancel; ebi('rn_apply').onclick = rn_apply; var ire = ebi('rn_re'), ifmt = ebi('rn_fmt'), ipre = ebi('rn_pre'), idel = ebi('rn_pdel'), inew = ebi('rn_pnew'), defp = '$lpad((tn),2,0). [(artist) - ](title).(ext)'; ire.value = sread('cpp_rn_re') || ''; ifmt.value = sread('cpp_rn_fmt') || ''; var presets = {}; presets[defp] = ['', defp]; presets = jread("rn_pre", presets); function spresets() { var keys = Object.keys(presets), o; keys.sort(); ipre.innerHTML = ''; for (var a = 0; a < keys.length; a++) { o = mknod('option'); o.setAttribute('value', keys[a]); o.textContent = keys[a]; ipre.appendChild(o); } } inew.onclick = function (e) { ev(e); modal.prompt(L.fr_pname, ifmt.value, function (name) { if (!name) return toast.warn(3, L.fr_aborted); presets[name] = [ire.value, ifmt.value]; jwrite('rn_pre', presets); spresets(); ipre.value = name; }); }; idel.onclick = function (e) { ev(e); delete presets[ipre.value]; jwrite('rn_pre', presets); spresets(); }; ipre.oninput = function () { var cfg = presets[ipre.value]; if (cfg) { ire.value = cfg[0]; ifmt.value = cfg[1]; } ifmt.oninput(); }; spresets(); ebi('rn_n_s').oninput = function () { r.n_s = parseInt(this.value || '1'); ifmt.oninput(); }; ebi('rn_n_d').oninput = function () { r.n_d = parseInt(this.value || '1'); ifmt.oninput(); }; ire.onkeydown = ifmt.onkeydown = function (e) { var k = (e.key || e.code) + ''; if (k == 'Escape' || k == 'Esc') return rn_cancel(); if (k.endsWith('Enter')) return rn_apply(); }; ire.oninput = ifmt.oninput = function (e) { var ptn = ire.value, fmt = ifmt.value, re = null; if (!fmt) return; try { if (ptn) re = new RegExp(ptn, r.cs ? 'i' : ''); } catch (ex) { return toast.err(5, esc('invalid regex:\n' + ex)); } toast.hide(); for (var a = 0; a < f.length; a++) { var m = re ? re.exec(f[a].ofn) : null, d = f[a].md, ok, txt = ''; d[".n.s"] = d["n.s"] = '' + (r.n_s + a); d[".n.d"] = d["n.d"] = '' + (r.n_d + s2d[a]); if (re && !m) { txt = 'regex did not match'; ok = false; } else { var ret = fmt_ren(m, d, fmt); ok = ret[0]; txt = ret[1]; } rn_ok(a, ok); f[a].inew.value = (ok ? '' : 'ERROR: ') + txt; } }; function rn_apply(e) { ev(e); swrite('cpp_rn_re', ire.value); swrite('cpp_rn_fmt', ifmt.value); if (r.win || r.slash) { var changed = 0; for (var a = 0; a < f.length; a++) { var ov = f[a].inew.value, nv = namesan(ov, r.win, r.slash); if (ov != nv) { f[a].inew.value = nv; changed++; } } if (changed) return modal.confirm(L.fr_nchg.format(changed), rn_apply_loop, null); } rn_apply_loop(); } function rn_apply_loop() { while (f.length && (!f[0].ok || f[0].ofn == f[0].inew.value)) f.shift(); if (!f.length) { toast.ok(2, 'rename OK'); treectl.goto(); return rn_cancel(); } var msg = esc(L.fr_busy.format(f.length, f[0].ofn)); msg += '\n
          ' + L.fs_abrt + ''; toast.show('inf r', 0, msg); var dst = base + uricom_enc(f[0].inew.value, false); function rename_cb() { if (this.status !== 201) { var msg = unpre(this.responseText); toast.err(9, L.fr_efail + msg); return; } if (r.sn != sn) return modal.confirm('WARNING: the rename was aborted'); f.shift().inew.value = '( OK )'; return rn_apply_loop(); } abrt_key = randstr(9); var xhr = new XHR(); xhr.open('POST', f[0].src + '?move=' + dst + '&akey=' + abrt_key, true); xhr.onload = xhr.onerror = rename_cb; xhr.send(); } }; r.delete = function (e) { var sel = msel.getsel(), sn = ++r.sn, vps = []; for (var a = 0; a < sel.length; a++) vps.push(sel[a].vp); if (!sel.length) return toast.err(3, L.fd_emore); ev(e); if (clgot(bdel, 'hide')) return toast.err(3, L.fd_eperm); function deleter(err) { var xhr = new XHR(), vp = vps.shift(); if (!vp) { if (err !== 'xbd') toast.ok(2, L.fd_ok); treectl.goto(); return; } toast.show('inf r', 0, esc(L.fd_busy.format(vps.length + 1, vp)), 'r'); xhr.open('POST', vp + '?delete', true); xhr.onload = xhr.onerror = delete_cb; xhr.send(); } function delete_cb() { if (this.status !== 200) { var msg = unpre(this.responseText); toast.err(9, L.fd_err + msg); return; } if (r.sn != sn) return modal.confirm('WARNING: the delete was aborted'); if (this.responseText.indexOf('deleted 0 files (and 0') + 1) { toast.err(9, L.fd_none); return deleter('xbd'); } deleter(); } var asks = r.qdel ? 1 : 2; if (dqdel === 0) asks -= 1; if (!asks) return deleter(); modal.confirm('
          ' + L.danger + '
          \n' + L.fd_warn1.format(vps.length) + '
            ' + uricom_adec(vps, true).join('') + '
          ', function () { if (asks === 1) return deleter(); modal.confirm(L.fd_warn2, deleter, null); }, null); }; r.cut = function (e) { var sel = msel.getsel(), stamp = Date.now(), vps = [stamp]; if (!sel.length) return toast.err(3, L.fc_emore); ev(e); if (clgot(bcut, 'hide')) return toast.err(3, L.fc_eperm); var els = [], griden = thegrid.en; for (var a = 0; a < sel.length; a++) { vps.push(sel[a].vp); if (sel.length < 100) try { if (griden) els.push(QS('#ggrid>a[ref="' + sel[a].id + '"]')); else els.push(ebi(sel[a].id).closest('tr')); clmod(els[a], 'fcut'); } catch (ex) { } } setTimeout(function () { try { for (var a = 0; a < els.length; a++) clmod(els[a], 'fcut', 1); } catch (ex) { } }, 1); r.ccp = false; r.clip = vps.slice(1); try { vps = JSON.stringify(vps); if (vps.length > 1024 * 1024) throw 'a'; swrite('fman_clip', vps); r.tx(stamp); if (sel.length) toast.inf(1.5, L.fc_ok.format(sel.length)); } catch (ex) { toast.warn(30, L.fc_warn.format(sel.length)); } }; r.cpy = function (e) { var sel = msel.getsel(), stamp = Date.now(), vps = [stamp, '//c']; if (!sel.length) return toast.err(3, L.fcp_emore); ev(e); var els = [], griden = thegrid.en; for (var a = 0; a < sel.length; a++) { vps.push(sel[a].vp); if (sel.length < 100) try { if (griden) els.push(QS('#ggrid>a[ref="' + sel[a].id + '"]')); else els.push(ebi(sel[a].id).closest('tr')); clmod(els[a], 'fcut'); } catch (ex) { } } setTimeout(function () { try { for (var a = 0; a < els.length; a++) clmod(els[a], 'fcut', 1); } catch (ex) { } }, 1); if (vps.length < 3) vps.pop(); r.ccp = true; r.clip = vps.slice(2); var prefix = sread("clip_uo") || location.origin; try { cliptxt(prefix + r.clip.join('\n' + prefix)); } catch (ex) {} try { vps = JSON.stringify(vps); if (vps.length > 1024 * 1024) throw 'a'; swrite('fman_clip', vps); r.tx(stamp); if (sel.length) toast.inf(1.5, L.fcc_ok.format(sel.length)); } catch (ex) { toast.warn(30, L.fcc_warn.format(sel.length)); } }; document.onpaste = function (e) { var xfer = e.clipboardData || window.clipboardData; if (!xfer || !xfer.files || !xfer.files.length) return; var files = []; for (var a = 0, aa = xfer.files.length; a < aa; a++) files.push(xfer.files[a]); clearTimeout(t_paste); if (!r.clip.length) return r.clip_up(files); var src = r.clip.length == 1 ? r.clip[0] : vsplit(r.clip[0])[0], msg = (r.ccp ? L.fcp_both_m : L.fp_both_m).format(r.clip.length, src, files.length); modal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, (r.ccp ? L.fcp_both_b : L.fp_both_b)); }; r.clip_up = function (files) { goto_up2k(); var good = [], nil = [], bad = []; for (var a = 0, aa = files.length; a < aa; a++) { var fobj = files[a], dst = good; try { if (fobj.size < 1) dst = nil; } catch (ex) { dst = bad; } dst.push([fobj, fobj.name]); } var doit = function (is_img) { jwrite('fman_clip', [Date.now()]); r.clip = []; var x = up2k.uc.ask_up; if (is_img) up2k.uc.ask_up = false; up2k.gotallfiles[0](good, nil, bad, up2k.gotallfiles.slice(1)); up2k.uc.ask_up = x; }; if (good.length != 1) return doit(); var fn = good[0][1], ofs = fn.lastIndexOf('.'); // stop linux-chrome from adding the fs-path into the setTimeout(function () { modal.prompt(L.fp_name, fn, function (v) { good[0][1] = v; doit(true); }, null, null, 0, ofs > 0 ? ofs : undefined); }, 1); }; r.d_paste = function () { // gets called before onpaste; defer clearTimeout(t_paste); t_paste = setTimeout(r.paste, 50); }; r.paste = function () { if (!r.clip.length) return toast.err(5, L.fp_ecut); if (clgot(bpst, 'hide')) return toast.err(3, L.fp_eperm); var html = [ '
          ', '', '', '   src: ' + esc(r.clip[0].replace(/[^/]+$/, '')), '
          ', '

          ', '
          ', '', ], sn = ++r.sn, ui = false, f = [], indir = [], srcdir = vsplit(r.clip[0])[0], links = QSA('#files tbody td:nth-child(2) a'); r.f = f; for (var a = 0, aa = links.length; a < aa; a++) indir.push(uricom_dec(vsplit(noq_href(links[a]))[1])); for (var a = 0; a < r.clip.length; a++) { var t = { 'ok': true, 'src': r.clip[a], 'dst': uricom_dec(r.clip[a].split('/').pop()), }; f.push(t); for (var b = 0; b < indir.length; b++) if (t.dst == indir[b]) { t.ok = false; ui = true; } html.push(''); } function paster() { var t = f.shift(); if (!t) { toast.ok(2, r.ccp ? L.fcp_ok : L.fp_ok); treectl.goto(); r.tx(srcdir); return; } if (!t.dst) return paster(); var msg = esc((r.ccp ? L.fcp_busy : L.fp_busy).format(f.length + 1, uricom_dec(t.src))); msg += '\n' + L.fs_abrt + ''; toast.show('inf r', 0, msg); var xhr = new XHR(), act = r.ccp ? '?copy=' : '?move=', dst = get_evpath() + uricom_enc(t.dst); abrt_key = randstr(9); xhr.open('POST', t.src + act + dst + '&akey=' + abrt_key, true); xhr.onload = xhr.onerror = paste_cb; xhr.send(); } function paste_cb() { if (this.status !== 201) { var msg = unpre(this.responseText); toast.err(9, (r.ccp ? L.fcp_err : L.fp_err) + msg); return; } if (r.sn != sn) return modal.confirm('WARNING: the paste was aborted'); paster(); } function okgo() { paster(); jwrite('fman_clip', [Date.now()]); } if (!ui) { var src = []; for (var a = 0; a < f.length; a++) src.push(f[a].src); return modal.confirm((r.ccp ? L.fcp_confirm : L.fp_confirm).format(f.length) + '
            ' + uricom_adec(src, true).join('') + '
          ', okgo, null); } var rui = ebi('rui'); if (!rui) { rui = mknod('div', 'rui'); document.body.appendChild(rui); } html.push('
          ' + L.fr_lnew + '' + L.fr_lold + '
          '); rui.innerHTML = html.join('\n'); tt.att(rui); function rn_apply(e) { for (var a = 0; a < f.length; a++) if (!f[a].ok) { toast.err(30, L.fp_emore); return setcnmt(true); } rn_cancel(e); okgo(); } function rn_skip(e) { var o = QSA('#rn_f tr.ng'); for (var a = o.length - 1; a >= 0; a--) { var oo = o[a].querySelector('input'); oo.value = ''; oo.oninput.call(oo); } rn_apply(); } function rn_cancel(e) { ev(e); rui.parentNode.removeChild(rui); } ebi('rn_cancel').onclick = rn_cancel; ebi('rn_skip').onclick = rn_skip; ebi('rn_apply').onclick = rn_apply; var first_bad = 0; function setcnmt(sel) { var nbad = 0; for (var a = 0; a < f.length; a++) { if (f[a].ok) continue; if (!nbad) first_bad = a; nbad += 1; } ebi('cnmt').innerHTML = (r.ccp ? L.fcp_ename : L.fp_ename).format(nbad); if (sel && nbad) { var el = ebi('rn_new_' + first_bad); el.focus(); el.setSelectionRange(0, el.value.lastIndexOf('.'), "forward"); } } setcnmt(true); for (var a = 0; a < f.length; a++) (function (a) { var inew = ebi('rn_new_' + a); inew.onkeydown = function (e) { if (((e.key || e.code) + '').endsWith('Enter')) return rn_apply(); }; inew.oninput = function (e) { f[a].dst = this.value; f[a].ok = true; if (f[a].dst) for (var b = 0; b < indir.length; b++) if (indir[b] == this.value) f[a].ok = false; clmod(this.closest('tr'), 'ng', !f[a].ok); setcnmt(); }; })(a); } function onmsg(msg) { r.clip = null; var n = parseInt('' + msg), tries = 0; var fun = function () { if (n == msg && n > 1 && r.clip === null) { var fc = jread('fman_clip', []); if (!fc || !fc.length || fc[0] != n) { if (++tries > 10) return modal.alert(L.fp_etab); return setTimeout(fun, 100); } } r.render(); if (msg == get_evpath()) treectl.goto(msg); }; fun(); } if (r.bus) r.bus.onmessage = function (e) { onmsg(e ? e.data : 1) }; r.tx = function (msg) { if (!r.bus) return onmsg(msg); r.bus.postMessage(msg); r.bus.onmessage(); }; bcfg_bind(r, 'qdel', 'qdel', dqdel == 1); bren.onclick = r.rename; bdel.onclick = r.delete; bcut.onclick = r.cut; bcpy.onclick = r.cpy; bpst.onclick = r.paste; bshr.onclick = r.share; return r; })(); var showfile = (function () { var r = { 'nrend': 0, }; r.map = { '.asm': 'nasm', '.bas': 'basic', '.bat': 'batch', '.cxx': 'cpp', '.diz': 'ans', '.ex': 'elixir', '.exs': 'elixir', '.frag': 'glsl', '.h': 'c', '.hpp': 'cpp', '.htm': 'html', '.hxx': 'cpp', '.log': 'ans', '.m': 'matlab', '.moon': 'moonscript', '.nfo': 'ans', '.patch': 'diff', '.ps1': 'powershell', '.psm1': 'powershell', '.pl': 'perl', '.rs': 'rust', '.sh': 'bash', '.service': 'systemd', '.socket': 'systemd', '.timer': 'systemd', '.txt': 'ans', '.vb': 'vbnet', '.v': 'verilog', '.vert': 'glsl', '.vh': 'verilog', '.yml': 'yaml' }; r.nmap = { 'dockerfile': 'docker' }; var x = txt_ext + ' ans c cfg conf cpp cs css diff glsl go html ini java js json jsx kt kts latex less lisp lua makefile md nasm nim nix py r rss rb ruby sass scss sql svg swift tex toml ts vhdl xml yaml zig'; x = x.split(/ +/g); for (var a = 0; a < x.length; a++) if (!r.map["." + x[a]]) r.map["." + x[a]] = x[a]; r.sname = function (srch) { return srch.split(/[?&]doc=/)[1].split('&')[0]; }; if (window.og_fn) { var ext = og_fn.split(/\./g).pop(); if (r.map['.' + ext]) hist_replace(get_evpath() + '?doc=' + og_fn); } window.Prism = { 'manual': true }; var em = QS('#bdoc>pre'); if (em) em = [r.sname(location.search), location.hash, em.textContent]; else { var m = /[?&]doc=([^&]+)/.exec(location.search); if (m) { setTimeout(function () { r.show(uricom_dec(m[1]), true); }, 1); } } r.setstyle = function () { if (window.no_prism) return; qsr('#prism_css'); var el = mknod('link', 'prism_css'); el.rel = 'stylesheet'; el.href = SR + '/.cpr/w/deps/prism' + (light ? '' : 'd') + '.css?_=' + TS; document.head.appendChild(el); }; r.active = function () { return !!/[?&]doc=/.exec(location.search); }; r.getlang = function (fn) { fn = fn.toLowerCase(); var ext = fn.slice(fn.lastIndexOf('.')); return r.map[ext] || r.nmap[fn]; } r.addlinks = function () { r.files = []; var links = msel.getall(); for (var a = 0; a < links.length; a++) { var link = links[a], fn = link.vp.split('/').pop(), lang = r.getlang(fn); if (!lang) continue; r.files.push({ 'id': link.id, 'name': uricom_dec(fn) }); var ah = ebi(link.id), td = ah.closest('tr').getElementsByTagName('td')[0]; if (ah.textContent.endsWith('/')) continue; if (lang == 'ts' || (lang == 'md' && td.textContent != '-')) continue; td.innerHTML = '-txt-'; td.getElementsByTagName('a')[0].setAttribute('href', '?doc=' + fn); } r.mktree(); if (em) { if (r.taildoc || em[2] == '( size of textfile exceeds serverside limit )') r.show(em[0], true); else render(em); em = null; } }; r.tail = function (url, no_push) { r.abrt = new AbortController(); widget.setvis(); render([url, '', ''], no_push); var me = r.tail_id = Date.now(), wfp = ebi('wfp'), edoc = ebi('doc'), txt = ''; url = addq(url, 'tail=-' + r.tailnb); fetch(url, {'signal': r.abrt.signal}).then(function(rsp) { var ro = rsp.body.pipeThrough( new TextDecoderStream('utf-8', {'fatal': false}), {'signal': r.abrt.signal}).getReader(); var rf = function() { ro.read().then(function(v) { if (r.tail_id != me) return; var vt = v.done ? '\n*** lost connection to copyparty ***' : v.value; if (vt == '\x00') return rf(); txt += vt; var ofs = txt.length - r.tailnb; if (ofs > 0) { var ofs2 = txt.indexOf('\n', ofs); if (ofs2 >= ofs && ofs - ofs2 < 512) ofs = ofs2; txt = txt.slice(ofs); } var html = esc(txt); if (r.tailansi) html = r.ansify(html); edoc.innerHTML = html; if (r.tail2end) window.scrollTo(0, wfp.offsetTop - window.innerHeight); if (!v.done) rf(); }); }; if (r.tail_id == me) rf(); }); }; r.untail = function () { if (!r.abrt) return; r.abrt.abort(); r.abrt = null; r.tail_id = -1; widget.setvis(); }; r.show = function (url, no_push) { r.untail(); var xhr = new XHR(), m = /[?&](k=[^&#]+)/.exec(url); url = url.split('?')[0] + (m ? '?' + m[1] : ''); assert_vp(url); if (r.taildoc) return r.tail(url, no_push); xhr.url = url; xhr.fname = uricom_dec(url.split('/').pop()); xhr.no_push = no_push; xhr.ts = Date.now(); xhr.open('GET', url, true); xhr.onprogress = loading; xhr.onload = xhr.onerror = load_cb; xhr.send(); }; function loading(e) { if (e.total < 1024 * 256) return; var m = L.tv_load.format( esc(this.fname), f2f(e.loaded * 100 / e.total, 1), f2f(e.loaded / 1024 / 1024, 1), f2f(e.total / 1024 / 1024, 1)) if (!this.toasted) { this.toasted = 1; return toast.inf(573, m); } ebi('toastb').innerHTML = lf2br(m); } function load_cb(e) { if (this.toasted) toast.hide(); if (!xhrchk(this, L.tv_xe1, L.tv_xe2)) return; render([this.url, '', this.responseText], this.no_push); } function render(doc, no_push) { r.q = null; r.nrend++; var url = r.url = doc[0], lnh = doc[1], txt = doc[2], name = url.split('?')[0].split('/').pop(), tname = uricom_dec(name), lang = r.getlang(name), is_md = lang == 'md'; ebi('files').style.display = ebi('gfiles').style.display = ebi('lazy').style.display = ebi('pro').style.display = ebi('epi').style.display = 'none'; ebi('dldoc').setAttribute('href', url); ebi('editdoc').setAttribute('href', addq(url, 'edit')); ebi('editdoc').style.display = (has(perms, 'write') && (re_rw_edit.test(name) || has(perms, 'delete'))) ? '' : 'none'; var wr = ebi('bdoc'), nrend = r.nrend, defer = !Prism.highlightElement; var fun = function (el) { if (r.nrend != nrend) return; try { if (lnh.slice(0, 5) == '#doc.') sethash(lnh.slice(1)); el = el || QS('#doc>code'); Prism.highlightElement(el); if (el.className == 'language-ans' || (!lang && /\x1b\[[0-9;]{0,16}m/.exec(txt.slice(0, 4096)))) el.innerHTML = r.ansify(el.innerHTML); } catch (ex) { } } var skip_prism = !txt || txt.length > 1024 * 256; if (skip_prism) { fun = function (el) { }; is_md = false; } qsr('#doc'); var el = mknod('pre', 'doc'); el.setAttribute('tabindex', '0'); clmod(ebi('wrap'), 'doc', !is_md); if (is_md) { show_md(txt, name, el); } else { el.textContent = txt; el.innerHTML = '' + el.innerHTML + ''; if (!window.no_prism && !skip_prism) { if ((lang == 'conf' || lang == 'cfg') && ('\n' + txt).indexOf('\n# -*- mode: yaml -*-') + 1) lang = 'yaml'; el.className = 'prism linkable-line-numbers line-numbers language-' + lang; if (!defer) fun(el.firstChild); else import_js(SR + '/.cpr/w/deps/prism.js', function () { fun(); }); } if (!txt && r.wrap) el.className = 'wrap'; } wr.appendChild(el); wr.style.display = ''; set_tabindex(); wintitle(tname + ' \u2014 '); document.documentElement.scrollTop = 0; var hfun = no_push ? hist_replace : hist_push; hfun(get_evpath() + '?doc=' + name); // can't dk: server wants dk and js needs fk qsr('#docname'); el = mknod('span', 'docname'); el.textContent = tname; ebi('path').appendChild(el); r.updtree(); treectl.textmode(true); tree_scrollto(); } r.ansify = function (html) { var ctab = (light ? 'bfbfbf d30253 497600 b96900 006fbb a50097 288276 2d2d2d 9f9f9f 943b55 3a5600 7f4f00 00507d 683794 004343 000000' : '404040 f03669 b8e346 ffa402 02a2ff f65be3 3da698 d2d2d2 606060 c75b79 c8e37e ffbe4a 71cbff b67fe3 9cf0ed ffffff').split(/ /g), src = html.split(/\x1b\[/g), out = [''], fg = 7, bg = null, bfg = 0, bbg = 0, inv = 0, bold = 0; for (var a = 0; a < src.length; a++) { var m = /^([0-9;]+)m/.exec(src[a]); if (!m) { if (a) out.push('\x1b['); out.push(src[a]); continue; } var cs = m[1].split(/;/g), txt = src[a].slice(m[1].length + 1); for (var b = 0; b < cs.length; b++) { var c = parseInt(cs[b]); if (c == 0) { fg = 7; bg = null; bfg = bbg = bold = inv = 0; } if (c == 1) bfg = bold = 1; if (c == 7) inv = 1; if (c == 22) bfg = bold = 0; if (c == 27) inv = 0; if (c >= 30 && c <= 37) fg = c - 30; if (c >= 40 && c <= 47) bg = c - 40; if (c >= 90 && c <= 97) { fg = c - 90; bfg = 1; } if (c >= 100 && c <= 107) { bg = c - 100; bbg = 1; } } var cfg = fg, cbg = bg; if (inv) { cbg = fg; cfg = bg || 0; } var s = '' + txt); } return out.join(''); }; r.ppj = function (e) { ebi(e); try { r.ppj2(); } catch (ex) { toast.err(10, '' + ex); } }; r.ppj2 = function () { var btn = ebi('dldoc'), el = ebi('doc'), t = el.textContent.trim(), jo = JSON.parse(t), jt = JSON.stringify(jo, null, t.indexOf('\n') + 1 ? 0 : 2); el.textContent = jt; el.innerHTML = '' + el.innerHTML + ''; try { el = QS('#doc>code'); el.className = 'language-json'; Prism.highlightElement(el); } catch (ex) { } btn.setAttribute('download', ebi('docname').innerHTML); btn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jt)); }; r.mktree = function () { var top = get_evpath().slice(SR.length), crumbs = linksplit(top).join('/'), html = ['
        • ' + L.tv_lst + '
          ' + crumbs + '
        • ']; for (var a = 0; a < r.files.length; a++) { var file = r.files[a]; html.push('
        • ' + esc(file.name) + ''); } ebi('docul').innerHTML = html.join('\n'); }; r.updtree = function () { var fn = QS('#path span:last-child'), lis = QSA('#docul li a'), sels = msel.getsel(), actsel = false; fn = fn ? fn.textContent : ''; for (var a = 0, aa = lis.length; a < aa; a++) { var lin = lis[a].textContent, sel = false; for (var b = 0; b < sels.length; b++) if (vsplit(sels[b].vp)[1] == lin) sel = true; clmod(lis[a], 'hl', lin == fn); clmod(lis[a], 'sel', sel); if (lin == fn && sel) actsel = true; } clmod(ebi('seldoc'), 'sel', actsel); }; r.tglsel = function () { var fn = ebi('docname').textContent; for (var a = 0; a < r.files.length; a++) if (r.files[a].name == fn) clmod(ebi(r.files[a].id).closest('tr'), 'sel', 't'); msel.selui(); }; r.tgltail = function () { if (!window.TextDecoderStream) { bcfg_set('taildoc', r.taildoc = false); return toast.err(10, L.tail_2old); } r.show(r.url, true); }; r.tglwrap = function () { r.show(r.url, true); }; var bdoc = ebi('bdoc'); bdoc.className = 'line-numbers'; bdoc.innerHTML = ( '\n' + '
        • ' ); ebi('xdoc').onclick = function () { r.untail(); thegrid.setvis(true); bcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail); }; ebi('dldoc').setAttribute('download', ''); ebi('prevdoc').onclick = function () { tree_neigh(-1); }; ebi('nextdoc').onclick = function () { tree_neigh(1); }; ebi('seldoc').onclick = r.tglsel; ebi('ppjdoc').onclick = r.ppj; bcfg_bind(r, 'wrap', 'wrapdoc', true, r.tglwrap); bcfg_bind(r, 'taildoc', 'taildoc', false, r.tgltail); bcfg_bind(r, 'tail2end', 'tail2end', true); bcfg_bind(r, 'tailansi', 'tailansi', false, r.tgltail); r.tailnb = ebi('tailnb').value = icfg_get('tailnb', 131072); ebi('tailnb').oninput = function (e) { swrite('tailnb', r.tailnb = this.value); }; if (/[?&]tail\b/.exec(sloc0)) { clmod(ebi('taildoc'), 'on', 1); r.taildoc = true; } return r; })(); var thegrid = (function () { var lfiles = ebi('files'), gfiles = mknod('div', 'gfiles'); gfiles.style.display = 'none'; gfiles.innerHTML = ( '
          ' + ' ' + '+ ' + L.gt_chop + ': ' + ' ' + '+ ' + L.gt_sort + ': ' + '' + L.gt_name + ' ' + '' + L.gt_sz + ' ' + '' + L.gt_ts + ' ' + '' + L.gt_ext + '' + '
          ' + '
          ' ); lfiles.parentNode.insertBefore(gfiles, lfiles); var ggrid = ebi('ggrid'); var r = { 'sz': clamp(fcfg_get('gridsz', 10), 4, 80), 'ln': clamp(icfg_get('gridln', 3), 1, 7), 'isdirty': true, 'bbox': null }; var btnclick = function (e) { ev(e); var s = this.getAttribute('s'), z = this.getAttribute('z'), l = this.getAttribute('l'); if (z) return setsz(z > 0 ? r.sz * z : r.sz / (-z)); if (l) return setln(parseInt(l)); var t = lfiles.tHead.rows[0].cells; for (var a = 0; a < t.length; a++) if (t[a].getAttribute('name') == s) { t[a].click(); break; } r.setdirty(); }; var links = QSA('#ghead a'); for (var a = 0; a < links.length; a++) links[a].onclick = btnclick; r.setvis = function (force) { if (showfile.active()) { if (!force) return; hist_push(get_evpath() + (dk ? '?k=' + dk : '')); wintitle(); } lfiles = ebi('files'); gfiles = ebi('gfiles'); ggrid = ebi('ggrid'); var vis = has(perms, "read"); gfiles.style.display = vis && r.en ? '' : 'none'; lfiles.style.display = vis && !r.en ? '' : 'none'; clmod(ggrid, 'crop', r.crop); clmod(ggrid, 'nocrop', !r.crop); ebi('pro').style.display = ebi('epi').style.display = ebi('lazy').style.display = ebi('treeul').style.display = ebi('treepar').style.display = ''; ebi('bdoc').style.display = 'none'; clmod(ebi('wrap'), 'doc'); qsr('#docname'); if (treectl) treectl.textmode(false); if (filecols) filecols.uivis(); aligngriditems(); restore_scroll(); }; r.setdirty = function () { r.dirty = true; if (r.en) loadgrid(); else r.setvis(); }; function setln(v) { if (v) { r.ln += v; if (r.ln < 1) r.ln = 1; if (r.ln > 7) r.ln = v < 0 ? 7 : 99; swrite('gridln', r.ln); setTimeout(r.tippen, 20); } setcvar('--grid-ln', r.ln); } setln(); function setsz(v) { if (v !== undefined) { r.sz = clamp(v, 4, 80); swrite('gridsz', r.sz); setTimeout(r.tippen, 20); } setcvar('--grid-sz', r.sz + 'em'); aligngriditems(); } setsz(); function gclick1(e) { if (ctrl(e) && !treectl.csel && !r.sel) return true; return gclick.call(this, e, false); } function gclick2(e) { if (ctrl(e) || !r.sel) return true; return gclick.call(this, e, true); } function gclick(e, dbl) { var oth = ebi(this.getAttribute('ref')), qhref = this.getAttribute('href'), href = qhref.split('?')[0], fid = oth.getAttribute('id'), aplay = ebi('a' + fid), atext = ebi('t' + fid), is_txt = atext && !/\.ts$/.test(href) && showfile.getlang(href), is_img = img_re.test(href), is_dir = href.endsWith('/'), is_srch = !!ebi('unsearch'), in_tree = is_dir && treectl.find(oth.textContent.slice(0, -1)), have_sel = QS('#files tr.sel'), td = oth.closest('td').nextSibling, tr = td.parentNode; if (!is_srch && ((r.sel && !dbl && !ctrl(e)) || (treectl.csel && (e.shiftKey || ctrl(e))))) { td.onclick.call(td, e); if (e.shiftKey) return r.loadsel(); clmod(this, 'sel', clgot(tr, 'sel')); } else if (in_tree && !have_sel) in_tree.click(); else if (oth.hasAttribute('download')) oth.click(); else if (aplay && (r.vau || !is_img)) aplay.click(); else if (is_dir && !have_sel) treectl.reqls(qhref, true); else if (is_txt && !has(['md', 'htm', 'html'], is_txt)) atext.click(); else if (!is_img && have_sel) window.open(qhref, '_blank'); else { if (!dbl) return true; setTimeout(function () { r.sel = true; }, 1); r.sel = false; this.click(); } ev(e); } r.imshow = function (url) { var sel = '#ggrid>a' if (!thegrid.en) { thegrid.bagit('#files'); sel = '#files a[id]'; } var ims = QSA(sel); for (var a = 0, aa = ims.length; a < aa; a++) { var iu = ims[a].getAttribute('href').split('?')[0].split('/').slice(-1)[0]; if (iu == url) return ims[a].click(); } baguetteBox.hide(); }; r.loadsel = function () { if (r.dirty) return; var ths = QSA('#ggrid>a'); for (var a = 0, aa = ths.length; a < aa; a++) { var tr = ebi(ths[a].getAttribute('ref')).closest('tr'), cl = tr.className || ''; if (noq_href(ths[a]).endsWith('/')) cl += ' dir'; ths[a].className = cl; } var sp = ['unsearch', 'moar']; for (var a = 0; a < sp.length; a++) (function (a) { var o = QS('#ggrid a[ref="' + sp[a] + '"]'); if (o) o.onclick = function (e) { ev(e); ebi(sp[a]).click(); }; })(a); }; r.tippen = function () { var els = QSA('#ggrid>a>span'), aa = els.length; if (!aa) return; var cs = window.getComputedStyle(els[0]), fs = parseFloat(cs.lineHeight), pad = parseFloat(cs.paddingTop), pels = [], todo = []; for (var a = 0; a < aa; a++) { var vis = Math.round((els[a].offsetHeight - pad) / fs), all = Math.round((els[a].scrollHeight - pad) / fs), par = els[a].parentNode; pels.push(par); todo.push(vis < all ? par.getAttribute('ttt') : null); } for (var a = 0; a < todo.length; a++) { if (todo[a]) pels[a].setAttribute('tt', todo[a]); else pels[a].removeAttribute('tt'); } tt.att(ggrid); }; function loadgrid() { if (have_webp === null || have_jxl === null) return setTimeout(loadgrid, 50); r.setvis(); if (!r.dirty) return r.loadsel(); if (dcrop.startsWith('f') || !sread('gridcrop')) bcfg_upd_ui('gridcrop', r.crop = ('y' == dcrop.slice(-1))); if (dth3x.startsWith('f') || !sread('grid3x')) bcfg_upd_ui('grid3x', r.x3 = ('y' == dth3x.slice(-1))); var html = [], svgs = new Set(), max_svgs = CHROME ? 500 : 5000, need_ext = !r.thumbs || !!ext_th, use_ext_th = r.thumbs && ext_th, files = QSA('#files>tbody>tr>td:nth-child(2) a[id]'); for (var a = 0, aa = files.length; a < aa; a++) { var ao = files[a], ohref = esc(ao.getAttribute('href')), href = ohref.split('?')[0], ext = '', ext0 = '', name = uricom_dec(vsplit(href)[1]), ref = ao.getAttribute('id'), isdir = href.endsWith('/'), ac = isdir ? ' class="dir"' : '', ihref = ohref; if (need_ext && href != "#") { var ar = href.split('.'); if (ar.length > 1) ar.shift(); ar.reverse(); ext0 = ar[0]; for (var b = 0; b < Math.min(2, ar.length); b++) { if (ar[b].length > 7) break; ext = ext ? (ar[b] + '.' + ext) : ar[b]; } if (!ext) ext = 'unk'; } if (use_ext_th && (ext_th[ext] || ext_th[ext0])) { ihref = ext_th[ext] || ext_th[ext0]; } else if (r.thumbs) { ihref = addq(ihref, 'th=' + ( have_jxl ? 'x' : have_webp ? 'w' : 'j' )); if (!r.crop) ihref += 'f'; if (r.x3) ihref += '3'; if (href == "#") ihref = SR + '/.cpr/ico/' + (ref == 'moar' ? '++' : 'exit'); } else if (isdir) { ihref = SR + '/.cpr/ico/folder'; } else { if (!svgs.has(ext)) { if (svgs.size < max_svgs) svgs.add(ext); else ext = "unk"; } ihref = SR + '/.cpr/ico/' + ext; } ihref = addq(ihref, 'cache=i&_=' + ACB + TS); if (CHROME) ihref += "&raster"; html.push('' + ao.innerHTML + ''); } ggrid.innerHTML = html.join('\n'); clmod(ggrid, 'crop', r.crop); clmod(ggrid, 'nocrop', !r.crop); var srch = ebi('unsearch'), gsel = ebi('gridsel'); gsel.style.display = srch ? 'none' : ''; if (srch && r.sel) gsel.click(); var ths = QSA('#ggrid>a'); for (var a = 0, aa = ths.length; a < aa; a++) { ths[a].ondblclick = gclick2; ths[a].onclick = gclick1; } r.dirty = false; r.bagit('#ggrid'); r.loadsel(); aligngriditems(); setTimeout(r.tippen, 20); } r.bagit = function (isrc) { if (!window.baguetteBox) return; if (r.bbox) baguetteBox.destroy(); var br = baguetteBox.run(isrc, { noScrollbars: true, duringHide: r.onhide, afterShow: function () { r.bbox_opts.refocus = true; }, captions: function (g, idx) { var h = '' + g; return '' + (idx + 1) + ' / ' + this.length + ' -- ' + esc(uricom_dec(h.split('/').pop())) + ''; }, onChange: function (i, maxIdx) { if (this[i].imageElement) { sethash('g' + this[i].imageElement.getAttribute('ref') + getsort()); } } }); r.bbox = true; r.bbox_opts = br[1]; }; r.onhide = function () { afilt.apply(); if (!thegrid.ihop) return; try { var el = QS('#ggrid a[ref="' + location.hash.slice(2) + '"]'), f = function () { try { el.focus(); } catch (ex) { } }; f(); setTimeout(f, 10); setTimeout(f, 100); setTimeout(f, 200); // thx fullscreen api if (ANIM) { clmod(el, 'glow', 1); setTimeout(function () { try { clmod(el, 'glow'); } catch (ex) { } }, 600); } r.bbox_opts.refocus = false; } catch (ex) { console.log('ihop:', ex); } }; r.set_crop = function (en) { if (!dcrop.startsWith('f')) return r.setdirty(); r.crop = dcrop.endsWith('y'); bcfg_upd_ui('gridcrop', r.crop); if (r.crop != en) toast.warn(10, L.ul_btnlk); }; r.set_x3 = function (en) { if (!dth3x.startsWith('f')) return r.setdirty(); r.x3 = dth3x.endsWith('y'); bcfg_upd_ui('grid3x', r.x3); if (r.x3 != en) toast.warn(10, L.ul_btnlk); }; if (/[?&]grid\b/.exec(sloc0)) swrite('griden', /[?&]grid=0\b/.exec(sloc0) ? 0 : 1) if (/[?&]thumb\b/.exec(sloc0)) swrite('thumbs', /[?&]thumb=0\b/.exec(sloc0) ? 0 : 1) if (/[?&]imgs\b/.exec(sloc0)) { var n = /[?&]imgs=0\b/.exec(sloc0) ? 0 : 1; swrite('griden', n); if (n) swrite('thumbs', 1); } bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty); bcfg_bind(r, 'ihop', 'ihop', true); bcfg_bind(r, 'vau', 'gridvau', false); bcfg_bind(r, 'crop', 'gridcrop', !dcrop.endsWith('n'), r.set_crop); bcfg_bind(r, 'x3', 'grid3x', dth3x.endsWith('y'), r.set_x3); bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel); bcfg_bind(r, 'en', 'griden', dgrid, function (v) { v ? loadgrid() : r.setvis(true); pbar.onresize(); vbar.onresize(); }); ebi('wtgrid').onclick = ebi('griden').onclick; return r; })(); function th_onload(el) { el.style.height = ''; } function tree_scrollto(e) { ev(e); tree_scrolltoo('#treeul a.hl'); tree_scrolltoo('#docul a.hl'); } function tree_scrolltoo(q) { var act = QS(q), ul = act ? act.offsetParent : null; if (!ul) return; var ctr = ebi('tree'), em = parseFloat(getComputedStyle(act).fontSize), top = act.offsetTop + ul.offsetTop, min = top - 20 * em, max = top - (ctr.offsetHeight - 16 * em); if (ctr.scrollTop > min) ctr.scrollTop = Math.floor(min); else if (ctr.scrollTop < max) ctr.scrollTop = Math.floor(max); } function tree_neigh(n, ratelimit) { if (ratelimit && QS('.dumb_loader_thing') && Date.now() - treectl.busied < 5) return; var links = QSA(showfile.active() || treectl.texts ? '#docul li>a' : '#treeul li>a+a'); if (!links.length) { treectl.dir_cb = function () { tree_neigh(n); treectl.detree(); }; treectl.entree(null, true); return; } var act = -1; for (var a = 0, aa = links.length; a < aa; a++) { if (clgot(links[a], 'hl')) { act = a; break; } } if (act == -1 && !treectl.texts) return; act += n; if (act < 0) act = links.length - 1; if (act >= links.length) act = 0; if (showfile.active()) links[act].click(); else treectl.treego.call(links[act]); } function tree_up(justgo) { if (showfile.active()) return thegrid.setvis(true); var act = QS('#treeul a.hl'); if (!act) { treectl.dir_cb = function () { tree_up(justgo); treectl.detree(); }; treectl.entree(null, true); return; } if (act.previousSibling.textContent == '-') { act.previousSibling.click(); if (!justgo) return; } var a = act.parentNode.parentNode.parentNode.getElementsByTagName('a')[1]; if (a.parentNode.tagName == 'LI') a.click(); } function hkhelp() { var html = []; for (var ic = 0; ic < L.hks.length; ic++) { var c = L.hks[ic]; html.push(''); for (var a = 0; a < c.length; a++) try { if (!Array.isArray(c[a])) html.push(''); else { var t1 = c[a][0].replace('⇧', ''); html.push(''.format(t1, c[a][1])); } } catch (ex) { html.push(">>> " + c[a]); } html.push('
          ' + esc(c[a]) + '
          {0}{1}
          '); } qsr('#hkhelp'); var o = mknod('div', 'hkhelp'); o.innerHTML = html.join('\n'); document.body.appendChild(o); } var fselgen, fselctr; function fselfunw(e, ae, d, rem) { fselctr = 0; var gen = fselgen = Date.now(); if (rem) rem *= window.innerHeight; var selfun = function () { var el = ae[d + 'ElementSibling']; if (!el || gen != fselgen) return; el.focus(); var elh = el.offsetHeight; if (ctrl(e)) document.documentElement.scrollTop += (d == 'next' ? 1 : -1) * elh; if (e.shiftKey) { clmod(el, 'sel', 't'); msel.origin_tr(el); msel.selui(); } rem -= elh; if (rem > 0) { ae = document.activeElement; if (++fselctr % 5 && rem > elh * (FIREFOX ? 5 : 2)) selfun(); else setTimeout(selfun, 1); } } selfun(); } var konmai = 0, konmak = (function() { var u = "arrowup", d = "arrowdown", l = "arrowleft", r = "arrowright"; return [u, u, d, d, l, r, l, r, "b", "a", "enter"]; })(); var ahotkeys = function (e) { if (e.altKey || e.isComposing) return; if (QS('#bbox-overlay.visible') || modal.busy) return; var k = (e.key || e.code) + '', pos = -1, n, sh = e.shiftKey, ae = document.activeElement, aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : ''; if (k.startsWith('Key')) k = k.slice(3); else if (k.startsWith('Digit')) k = k.slice(5); var kl = k.toLowerCase(); if (dbg_kbd) console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which); if (konmai < 0) noop(); else if (konmak[konmai] != kl) konmai = konmai && kl == konmak[0] ? (konmai<3?konmai:1):0; else if (++konmai >= konmak.length) { konmai = -1; document.documentElement.scrollTop = 0; settheme.go(6); start_actx(); sfx_nice(); toast.inf(9, 'omega clearance granted', null, 'top'); setTimeout(function() { apply_perms(treectl.lsc); fileman.render(); }, 573); return ev(e); } if (k == 'Escape' || k == 'Esc') { ae && ae.blur(); tt.hide(); if (ebi('hkhelp')) return qsr('#hkhelp'); if (ebi('rcm').style.display) return rcm.hide(); if (toast.visible) return toast.hide(); if (ebi('rn_cancel')) return ebi('rn_cancel').click(); if (ebi('sh_abrt')) return ebi('sh_abrt').click(); if (QS('.opview.act')) return QS('#ops>a').click(); if (widget.is_open) return widget.close(); if (showfile.active()) return thegrid.setvis(true); if (!treectl.hidden) return treectl.detree(); if (QS('#unsearch')) return QS('#unsearch').click(); if (thegrid.en) return ebi('griden').click(); } var in_ftab = (aet == 'tr' || aet == 'td') && ae.closest('#files'); if (in_ftab) { var d = '', rem = 0; if (aet == 'td') ae = ae.closest('tr'); //ie11 if (k == 'ArrowUp' || k == 'Up') d = 'previous'; if (k == 'ArrowDown' || k == 'Down') d = 'next'; if (k == 'PageUp') { d = 'previous'; rem = 0.6; } if (k == 'PageDown') { d = 'next'; rem = 0.6; } if (d) { fselfunw(e, ae, d, rem); return ev(e); } if (k == 'Space' || k == 'Spacebar' || k == ' ') { clmod(ae, 'sel', 't'); msel.origin_tr(ae); msel.selui(); return ev(e); } } if (in_ftab || !aet || (ae && ae.closest('#ggrid'))) { if ((kl == 'a') && ctrl(e)) { var ntot = treectl.lsc.files.length + treectl.lsc.dirs.length, sel = msel.getsel(), all = msel.getall(); msel.evsel(e, sel.length < all.length); msel.origin_id(null); if (ntot > all.length) toast.warn(10, L.f_anota.format(all.length, ntot), L.f_anota); else if (toast.tag == L.f_anota) toast.hide(); return ev(e); } } if (ae && ae.closest('pre')) { if ((kl == 'a') && ctrl(e)) { var sel = document.getSelection(), ran = document.createRange(); sel.removeAllRanges(); ran.selectNode(ae.closest('pre')); sel.addRange(ran); return ev(e); } } if (k.endsWith('Enter') && ae && (ae.onclick || ae.hasAttribute('tabIndex'))) return ev(e) && ae.click() || true; if (aet && aet != 'a' && aet != 'tr' && aet != 'td' && aet != 'div' && aet != 'pre') return; if (k == '?') return hkhelp(); if (!sh && ctrl(e)) { var sel = window.getSelection && window.getSelection() || {}; sel = sel && !sel.isCollapsed && sel.direction != 'none'; if (kl == 'x') return fileman.cut(e); if (kl == 'c' && !sel) return fileman.cpy(e); if (kl == 'v') return fileman.d_paste(e); if (kl == 'k') return fileman.delete(e); return; } if (showfile.active()) { if (!sh && kl == 's') return showfile.tglsel() || true; if (!sh && kl == 'e' && ebi('editdoc').style.display != 'none') return ebi('editdoc').click() || true; if (sh && kl == 'j') return showfile.ppj(e) || true; } if (sh && kl != 'a' && kl != 'd') return; if (/^[0-9]$/.test(k)) pos = parseInt(k) * 0.1; if (pos !== -1) return seek_au_mul(pos) || true; if (kl == 'j') return prev_song() || true; if (kl == 'l') return next_song() || true; if (kl == 'p') return playpause() || true; n = kl == 'u' ? -10 : kl == 'o' ? 10 : 0; if (n !== 0) return seek_au_rel(n) || true; if (kl == 'y') return msel.getsel().length ? ebi('seldl').click() : showfile.active() ? ebi('dldoc').click() : dl_song(); n = kl == 'i' ? -1 : kl == 'k' ? 1 : 0; if (n !== 0) return tree_neigh(n, 1); if (kl == 'm') return tree_up(); if (kl == 'b') return treectl.hidden ? treectl.entree() : treectl.detree(); if (kl == 'g') return ebi('griden').click(); if (kl == 't') return ebi('thumbs').click(); if (kl == 'v') return ebi('filetree').click(); if (k == 'F2') return fileman.rename(); if (!treectl.hidden && (!sh || !thegrid.en)) { if (kl == 'a') return QS('#twig').click(); if (kl == 'd') return QS('#twobytwo').click(); } if (mp && mp.au && !mp.au.paused) { if (kl == 's') return sel_song(); } if (thegrid.en) { if (kl == 's') return ebi('gridsel').click(); if (kl == 'a') return QSA('#ghead a[z]')[0].click(); if (kl == 'd') return QSA('#ghead a[z]')[1].click(); } }; // search var search_ui = (function () { var sconf = [ [ L.s_sz, ["szl", "sz_min", L.s_s1, "14", ""], ["szu", "sz_max", L.s_s2, "14", ""] ], [ L.s_dt, ["dtl", "dt_min", L.s_d1, "14", "1997-08-15, 01:00"], ["dtu", "dt_max", L.s_d2, "14", "2020"] ], [ L.s_rd, ["path", "path", L.s_r1, "30", "windows -system32"] ], [ L.s_fn, ["name", "name", L.s_f1, "30", ".exe$"] ], [ L.s_ta, ["tags", "tags", L.s_t1, "30", "^irui$"] ], [ L.s_ad, ["adv", "adv", L.s_a1, "30", "key>=1A key<=2B .bpm>165"] ], [ L.s_ua, ["utl", "ut_min", L.s_u1, "14", "2007-04-08"], ["utu", "ut_max", L.s_u2, "14", "2038-01-19"] ] ]; var r = {}, trs = [], orig_url = null, orig_html = null, cap = 125; for (var a = 0; a < sconf.length; a++) { var html = ['
          ' + sconf[a][0] + '']; for (var b = 1; b < 3; b++) { var hn = "srch_" + sconf[a][b][0], csp = (sconf[a].length == 2) ? 2 : 1; html.push( '\n' + '\n' + '
          '); if (csp == 2) break; } html.push(''); trs.push(html); } var html = []; for (var a = 0; a < trs.length; a += 2) { html.push('' + (trs[a].concat(trs[a + 1])).join('\n') + '
          '); } html.push('
          raw
          '); ebi('srch_form').innerHTML = html.join('\n'); var o = QSA('#op_search input'); for (var a = 0; a < o.length; a++) { o[a].oninput = ev_search_input; o[a].onkeydown = ev_search_keydown; } function srch_msg(err, txt) { var o = ebi('srch_q'); o.textContent = txt; clmod(o, 'err', err); } var search_timeout, defer_timeout, search_in_progress = 0; function ev_search_input() { var v = unsmart(this.value), id = this.getAttribute('id'), is_txt = id.slice(-1) == 'v', is_chk = id.slice(-1) == 'c'; if (is_txt) { var chk = ebi(id.slice(0, -1) + 'c'); chk.checked = ((v + '').length > 0); } if (id != "q_raw") encode_query(); set_vq(); cap = 125; clearTimeout(defer_timeout); if (is_chk) return do_search(); defer_timeout = setTimeout(try_search, 2000); try_search(v); } function ev_search_keydown(e) { if ((e.key + '').endsWith('Enter')) do_search(); } function try_search(v) { if (Date.now() - search_in_progress > 30 * 1000) { clearTimeout(defer_timeout); clearTimeout(search_timeout); search_timeout = setTimeout(do_search, v && v.length < (MOBILE ? 4 : 3) ? 1000 : 500); } } function set_vq() { if (search_in_progress) return; var q = unsmart(ebi('q_raw').value), vq = ebi('files').getAttribute('q_raw'); srch_msg(false, (q == vq) ? '' : L.sm_prev + (vq ? vq : '(*)')); } function encode_query() { var q = ''; for (var a = 0; a < sconf.length; a++) { for (var b = 1; b < sconf[a].length; b++) { var k = sconf[a][b][0], chk = 'srch_' + k + 'c', vs = unsmart(ebi('srch_' + k + 'v').value), tvs = []; if (a == 1) vs = vs.trim().replace(/ +/, 'T'); while (vs) { vs = vs.trim(); if (!vs) break; var v = ''; if (vs.startsWith('"')) { var vp = vs.slice(1).split(/"(.*)/); v = vp[0]; vs = vp[1] || ''; while (v.endsWith('\\')) { vp = vs.split(/"(.*)/); v = v.slice(0, -1) + '"' + vp[0]; vs = vp[1] || ''; } } else { var vp = vs.split(/ +(.*)/); v = vp[0].replace(/\\"/g, '"'); vs = vp[1] || ''; } tvs.push(v); } if (!ebi(chk).checked) continue; for (var c = 0; c < tvs.length; c++) { var tv = tvs[c]; if (!tv.length) break; q += ' and '; if (k == 'adv') { q += tv.replace(/ +/g, " and ").replace(/([=!><]=?)/, " $1 "); continue; } if (k.length == 3) { q += k.replace(/l$/, ' >= ').replace(/u$/, ' <= ').replace(/^sz/, 'size').replace(/^dt/, 'date').replace(/^ut/, 'up_at') + tv; continue; } if (k == 'path' || k == 'name' || k == 'tags') { var not = ''; if (tv.slice(0, 1) == '-') { tv = tv.slice(1); not = 'not '; } if (tv.slice(0, 1) == '^') { tv = tv.slice(1); } else { tv = '*' + tv; } if (tv.slice(-1) == '$') { tv = tv.slice(0, -1); } else { tv += '*'; } if (tv.indexOf(' ') + 1) { tv = '"' + tv + '"'; } q += not + k + ' like ' + tv; } } } } ebi('q_raw').value = q.slice(5); } function do_search() { search_in_progress = Date.now(); srch_msg(false, L.sm_w8); clearTimeout(search_timeout); var xhr = new XHR(); xhr.open('POST', SR + '/?srch', true); xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.onload = xhr.onerror = xhr_search_results; xhr.ts = Date.now(); xhr.q_raw = unsmart(ebi('q_raw').value); xhr.send(JSON.stringify({ "q": xhr.q_raw, "n": cap })); } function xhr_search_results() { if (this.status !== 200) { var msg = hunpre(this.responseText); srch_msg(true, "http " + this.status + ": " + msg); search_in_progress = 0; return; } search_in_progress = 0; srch_msg(false, ''); var res = JSON.parse(this.responseText); r.render(res, this, true); } r.render = function (res, xhr, sort) { var tagord = res.tag_order; srch_msg(false, ''); if (sort) sortfiles(res.hits); var ofiles = ebi('files'); if (xhr && ofiles.getAttribute('ts') > xhr.ts) return; treectl.hide(); thegrid.setvis(true); var html = mk_files_header(tagord), seen = {}; html.push(''); html.push('-[❌] ' + L.sl_close + ' -- ' + L.sl_hits.format(res.hits.length) + (res.trunc ? ' -- ' + L.sl_moar + '' : '') + ''); for (var a = 0; a < res.hits.length; a++) { var r = res.hits[a], ts = parseInt(r.ts), sz = parseInt(r.sz), hsz = filesizefun(sz), rp = esc(uricom_dec(r.rp + '')), ext = rp.lastIndexOf('.') > 0 ? rp.split('.').pop().split('?')[0] : '%', id = 'f-' + ('00000000' + crc32(rp)).slice(-8); while (seen[id]) id += 'a'; seen[id] = 1; if (ext.length > 8) ext = '%'; var links = linksplit(r.rp + '', null, id).join('/'), nodes = ['-
          ' + links + '
          ' + hsz]; for (var b = 0; b < tagord.length; b++) { var k = esc(tagord[b]), v = r.tags[k] || ""; if (k == ".dur") { var sv = v ? s2ms(v) : ""; nodes[nodes.length - 1] += '' + sv; continue; } nodes.push(esc('' + v)); } nodes = nodes.concat([ext, unix2ui(ts)]); html.push(nodes.join('')); html.push(''); } if (!orig_html || orig_url != get_evpath()) { orig_html = ebi('files').innerHTML; orig_url = get_evpath(); } ofiles = set_files_html(html.join('\n')); ofiles.setAttribute("ts", xhr ? xhr.ts : 1); ofiles.setAttribute("q_raw", xhr ? xhr.q_raw : 'playlist'); set_vq(); mukey.render(); reload_browser(); filecols.set_style(['File Name']); if (xhr) sethash('q=' + uricom_enc(xhr.q_raw)); ebi('unsearch').onclick = unsearch; var m = ebi('moar'); if (m) m.onclick = moar; }; function unsearch(e) { ev(e); treectl.show(); set_files_html(orig_html); ebi('files').removeAttribute('q_raw'); orig_html = null; sethash(''); reload_browser(); } function moar(e) { ev(e); cap *= 2; do_search(); } return r; })(); function ev_load_m3u(e) { ev(e); var id = this.getAttribute('id').slice(1), url = ebi(id).getAttribute('href').split('?')[0]; modal.confirm(L.mm_m3u, function () { load_m3u(url); }, function () { if (has(perms, 'write') && has(perms, 'delete')) location = url + '?edit'; else showfile.show(url); } ); return false; } function load_m3u(url) { assert_vp(url); var xhr = new XHR(); xhr.open('GET', url, true); xhr.onload = render_m3u; xhr.url = url; xhr.send(); return false; } function render_m3u() { if (!xhrchk(this, L.tv_xe1, L.tv_xe2)) return; var evp = get_evpath(), m3u = this.responseText, xtd = m3u.slice(0, 12).indexOf('#EXTM3U') + 1, lines = m3u.replace(/\r/g, '\n').split('\n'), dur = 1, artist = '', title = '', ret = {'hits': [], 'tag_order': ['artist', 'title', '.dur'], 'trunc': false}; for (var a = 0; a < lines.length; a++) { var ln = lines[a].trim(); if (xtd && ln.startsWith('#')) { var m = /^#EXTINF:([0-9]+)[, ](.*)/.exec(ln); if (m) { dur = m[1]; title = m[2]; var ofs = title.indexOf(' - '); if (ofs > 0) { artist = title.slice(0, ofs); title = title.slice(ofs + 3); } } continue; } if (ln.indexOf('.') < 0) continue; var n = ret.hits.length + 1, url = ln; if (url.indexOf(':\\')) // C:\ url = url.split(/\\/g).pop(); url = url.replace(/\\/g, '/'); url = uricom_enc(url).replace(/%2f/gi, '/') if (!url.startsWith('/')) url = vjoin(evp, url); ret.hits.push({ "ts": 946684800 + n, "sz": 100000 + n, "rp": url, "tags": {".dur": dur, "artist": artist, "title": title} }); dur = 1; artist = title = ''; } search_ui.render(ret, null, false); sethash('m3u=' + this.url.split('?')[0].split('/').pop()); goto(); var el = QS('#files>tbody>tr.au>td>a.play'); if (el) el.click(); } function aligngriditems() { if (!treectl) return; var ggrid = ebi('ggrid'), em2px = parseFloat(getComputedStyle(ggrid).fontSize), gridsz = 10; try { gridsz = cprop('--grid-sz').slice(0, -2); } catch (ex) { } var gridwidth = ggrid.clientWidth, griditemcount = ggrid.children.length, totalgapwidth = em2px * griditemcount; if (/b/.test(themen + '')) totalgapwidth *= 2.8; var val, st = ggrid.style; if (((griditemcount * em2px) * gridsz) + totalgapwidth < gridwidth) { val = 'left'; } else { val = treectl.hidden ? 'center' : 'space-between'; } if (st.justifyContent != val) st.justifyContent = val; } onresize100.add(aligngriditems); var filecolwidth = (function () { var lastwidth = -1; return function () { var vw = window.innerWidth / parseFloat(getComputedStyle(document.body)['font-size']), w = Math.floor(vw - 2); if (w == lastwidth) return; lastwidth = w; setcvar('--file-td-w', w + 'em'); } })(); onresize100.add(filecolwidth, true); var treectl = (function () { var r = { "hidden": true, "sb_msg": false, "ls_cb": null, "dir_cb": tree_scrollto, "pdir": [] }, entreed = false, fixedpos = false, prev_atop = null, prev_winh = null, mentered = null, treesz = clamp(icfg_get('treesz', 16), 10, 50); if (/[?&]dlni\b/.exec(sloc0)) swrite('dlni', /[?&]dlni=0\b/.exec(sloc0) ? 0 : 1); var resort = function () { ENATSORT = NATSORT && clgot(ebi('nsort'), 'on'); treectl.gentab(get_evpath(), treectl.lsc); }; bcfg_bind(r, 'ireadme', 'ireadme', true); bcfg_bind(r, 'idxh', 'idxh', idxh, setidxh); bcfg_bind(r, 'dyn', 'dyntree', true, onresize); bcfg_bind(r, 'csel', 'csel', dgsel); bcfg_bind(r, 'dsel', 'dsel', !MOBILE); bcfg_bind(r, 'dlni', 'dlni', dlni, resort); bcfg_bind(r, 'dots', 'dotfiles', see_dots, function (v) { r.goto(); setck('dots=' + (v ? 'y' : '')); }); bcfg_bind(r, 'utctid', 'utctid', dutc, function (v) { window.unix2ui = v ? unix2iso : unix2iso_localtime; resort(); }); bcfg_bind(r, 'nsort', 'nsort', dnsort, resort); bcfg_bind(r, 'dir1st', 'dir1st', true, resort); setwrap(bcfg_bind(r, 'wtree', 'wraptree', true, setwrap)); setwrap(bcfg_bind(r, 'parpane', 'parpane', true, onscroll)); bcfg_bind(r, 'htree', 'hovertree', false, reload_tree); bcfg_bind(r, 'ask', 'bd_ask', MOBILE && FIREFOX); ebi('bd_lim').value = r.lim = icfg_get('bd_lim'); ebi('bd_lim').oninput = function (e) { var n = parseInt(this.value); swrite('bd_lim', r.lim = (isNum(n) ? n : 0) || 1000); }; r.nvis = r.lim; ldks = jread('dks', []); for (var a = ldks.length - 1; a >= 0; a--) { var s = ldks[a], o = s.lastIndexOf('?'); dks[s.slice(0, o)] = s.slice(o + 1); } function setwrap(v) { clmod(ebi('tree'), 'nowrap', !v); reload_tree(); } setwrap(r.wtree); function setidxh(v) { if (!v == !/\bidxh=y\b/.exec('' + document.cookie)) return; setck('idxh=' + (v ? 'y' : 'n')); } setidxh(r.idxh); r.entree = function (e, nostore) { ev(e); entreed = true; if (!nostore) swrite('entreed', 'tree'); get_tree("", get_evpath(), true); r.show(); } r.show = function () { r.hidden = false; if (!entreed) { ebi('path').style.display = nonav ? 'none' : 'inline-block'; return; } ebi('path').style.display = 'none'; ebi('tree').style.display = 'block'; window.addEventListener('scroll', onscroll); window.addEventListener('resize', onresize); onresize(); aligngriditems(); }; r.detree = function (e, nw) { ev(e); entreed = false; if (!nw) swrite('entreed', 'na'); r.hide(); if (!nonav) ebi('path').style.display = ''; }; r.hide = function () { r.hidden = true; ebi('path').style.display = 'none'; ebi('tree').style.display = 'none'; ebi('wrap').style.marginLeft = ''; window.removeEventListener('resize', onresize); window.removeEventListener('scroll', onscroll); aligngriditems(); }; function unmenter() { if (mentered) { mentered.style.position = ''; mentered = null; } } r.textmode = function (ya) { var chg = !r.texts != !ya; r.texts = ya; ebi('docul').style.display = ya ? '' : 'none'; ebi('treeul').style.display = ebi('treepar').style.display = ya ? 'none' : ''; clmod(ebi('filetree'), 'on', ya); if (chg) tree_scrollto(); }; ebi('filetree').onclick = function (e) { ev(e); r.textmode(!r.texts); }; r.textmode(false); function onscroll() { unmenter(); onscroll2(); } function onscroll2() { if (!entreed || r.hidden || document.visibilityState == 'hidden') return; var tree = ebi('tree'), wrap = ebi('wrap'), wraptop = null, atop = wrap.getBoundingClientRect().top, winh = window.innerHeight, parp = ebi('treepar'), y = tree.scrollTop, w = tree.offsetWidth; if (atop !== prev_atop || winh !== prev_winh) wraptop = Math.floor(wrap.offsetTop); if (r.parpane && r.pdir.length && w != r.pdirw) { r.pdirw = w; compy(); } if (!r.parpane || !r.pdir.length || y >= r.pdir.slice(-1)[0][0] || y <= r.pdir[0][0]) { clmod(parp, 'off', 1); r.pdirh = null; } else { var h1 = [], h2 = [], els = []; for (var a = 0; a < r.pdir.length; a++) { if (r.pdir[a][0] > y) break; var e2 = r.pdir[a][1], e1 = e2.previousSibling; h1.push('
        • ' + e1.outerHTML + e2.outerHTML + '
            '); h2.push('
        • '); els.push([e1, e2]); } h1 = h1.join('\n') + h2.join('\n'); if (h1 != r.pdirh) { r.pdirh = h1; parp.innerHTML = h1; clmod(parp, 'off'); var els = QSA('#treepar a'); for (var a = 0, aa = els.length; a < aa; a++) els[a].onclick = bad_proxy; } y = ebi('treeh').offsetHeight; if (!fixedpos) y += tree.offsetTop - yscroll(); y = (y - 3) + 'px'; if (parp.style.top != y) parp.style.top = y; } if (wraptop === null) return; prev_atop = atop; prev_winh = winh; if (fixedpos && atop >= 0) { tree.style.position = 'absolute'; tree.style.bottom = ''; fixedpos = false; } else if (!fixedpos && atop < 0) { tree.style.position = 'fixed'; tree.style.height = 'auto'; fixedpos = true; } if (fixedpos) { tree.style.top = Math.max(0, parseInt(atop)) + 'px'; } else { var top = Math.max(0, wraptop), treeh = winh - atop; tree.style.top = top + 'px'; tree.style.height = treeh < 10 ? '' : Math.floor(treeh) + 'px'; } } timer.add(onscroll2, true); function onresize(e) { if (!entreed || r.hidden) return; var q = '#tree', nq = -3; while (r.dyn) { nq++; q += '>ul>li'; if (!QS(q)) break; } nq = Math.max(nq, get_evpath().split('/').length - 2); var iw = (treesz + Math.max(0, nq)), w = iw + 'em', w2 = (iw + 2) + 'em'; setcvar('--nav-sz', w); ebi('tree').style.width = w; ebi('wrap').style.marginLeft = w2; onscroll(); } r.find = function (txt) { var ta = QSA('#treeul a.hl+ul>li>a+a'); for (var a = 0, aa = ta.length; a < aa; a++) if (ta[a].textContent == txt) return ta[a]; }; r.goto = function (url, push, back) { if (!url || !url.startsWith('/')) url = get_evpath() + (url || ''); get_tree("", url, true); r.reqls(url, push, back); }; function get_tree(top, dst, rst) { var xhr = new XHR(), m = /[?&](k=[^&#]+)/.exec(dst), k = m ? '&' + m[1] : dk ? '&k=' + dk : ''; xhr.top = top; xhr.dst = dst; xhr.rst = rst; xhr.ts = r.busied = Date.now(); xhr.open('GET', addq(dst, 'tree=' + top + (r.dots ? '&dots' : '') + k), true); xhr.onload = xhr.onerror = r.recvtree; xhr.send(); enspin('t'); } r.recvtree = function () { if (!xhrchk(this, L.tl_xe1, L.tl_xe2)) return; try { var res = JSON.parse(this.responseText); } catch (ex) { 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) { if (r.dots) return; 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) { var cur = ebi('treeul').getAttribute('ts'); if (cur && parseInt(cur) > ts + 20 && QS('#treeul>li>a+a')) { console.log("reject tree; " + cur + " / " + (ts - cur)); return; } ebi('treeul').setAttribute('ts', ts); if (SR && !top0) { var x = SR.slice(1).split('/'); while (x[0]) { res = res['k' + x.shift()]; if (!res) throw 'invalid --rp-loc (or bug?)'; } } var top = (top0 == '.' ? dst : top0).split('?')[0], name = uricom_dec(top.split('/').slice(-2)[0]), rtop = top.replace(/^\/+/, ""), html = parsetree(res, rtop.slice(SR.length)); if (!top0) { html = '
        • -[root]\n
            ' + html; if (rst || !ebi('treeul').getElementsByTagName('li').length) ebi('treeul').innerHTML = html + '
        • '; } else { html = '-' + esc(name) + "\n
            \n" + html + "
          "; var links = QSA('#treeul a+a'); for (var a = 0, aa = links.length; a < aa; a++) { if (links[a].getAttribute('href').split('?')[0] == top) { var o = links[a].parentNode; if (!o.getElementsByTagName('li').length) o.innerHTML = html; } } } qsr('#dlt_t'); try { QS('#treeul>li>a+a').textContent = '[root]'; } catch (ex) { console.log('got no root yet'); r.dir_cb = null; return; } reload_tree(); var fun = r.dir_cb; if (fun) { r.dir_cb = null; try { fun(); } catch (ex) { console.log("dir_cb failed", ex); } } }; function reload_tree() { var cevp = get_evpath(), cdir = r.nextdir || uricom_dec(cevp), links = QSA('#treeul a+a'), nowrap = QS('#tree.nowrap') && QS('#hovertree.on'), act = null; for (var a = 0, aa = links.length; a < aa; a++) { var qhref = links[a].getAttribute('href'), ehref = qhref.split('?')[0], href = uricom_dec(ehref), cl = ''; if (dk && ehref == cevp && !/[?&]k=/.exec(qhref)) links[a].setAttribute('href', addq(qhref, 'k=' + dk)); if (href == cdir) { act = links[a]; cl = 'hl'; } else if (cdir.startsWith(href)) { cl = 'par'; } links[a].className = cl; links[a].onclick = r.treego; links[a].onmouseenter = nowrap ? menter : null; links[a].onmouseleave = nowrap ? mleave : null; } links = QSA('#treeul li>a:first-child'); for (var a = 0, aa = links.length; a < aa; a++) { links[a].setAttribute('dst', links[a].nextSibling.getAttribute('href')); links[a].onclick = treegrow; } ebi('tree').onscroll = nowrap ? unmenter : null; r.pdir = []; try { while (act) { r.pdir.unshift([-1, act]); act = act.parentNode.parentNode.closest('li').querySelector('a:first-child+a'); } } catch (ex) { } r.pdir.shift(); r.pdirw = -1; onresize(); } function compy() { for (var a = 0; a < r.pdir.length; a++) r.pdir[a][0] = r.pdir[a][1].offsetTop; var ofs = 0; for (var a = 0; a < r.pdir.length - 1; a++) { ofs += r.pdir[a][1].offsetHeight + 1; r.pdir[a + 1][0] -= ofs; } } function menter(e) { var p = this.offsetParent, pp = p.offsetParent, ppy = pp.offsetTop, y = this.offsetTop + p.offsetTop + ppy - p.scrollTop - pp.scrollTop - (ppy ? document.documentElement.scrollTop : 0); this.style.top = y + 'px'; this.style.position = 'fixed'; mentered = this; } function mleave(e) { this.style.position = ''; mentered = null; } function bad_proxy(e) { if (ctrl(e)) return true; ev(e); var dst = this.getAttribute('dst'), k = dst ? 'dst' : 'href', v = dst ? dst : this.getAttribute('href'), els = QSA('#treeul a'); for (var a = 0, aa = els.length; a < aa; a++) if (els[a].getAttribute(k) === v) return els[a].click(); } r.treego = function (e) { if (ctrl(e)) return true; ev(e); if (this.className == 'hl' && this.previousSibling.textContent == '-') { treegrow.call(this.previousSibling, e); return; } var href = this.getAttribute('href'); r.reqls(href, true); r.dir_cb = tree_scrollto; thegrid.setvis(true); clmod(this, 'ld', 1); } r.reqls = function (url, hpush, back, hydrate) { if (IE && !history.pushState) return location = url; var xhr = new XHR(), m = /[?&](k=[^&#]+)/.exec(url), k = m ? '&' + m[1] : dk ? '&k=' + dk : '', uq = (r.dots ? '&dots' : '') + k; if (rtt !== null) uq += '&rtt=' + rtt; xhr.top = url.split('?')[0]; xhr.back = back xhr.hpush = hpush; xhr.hydrate = hydrate; xhr.ts = r.busied = Date.now(); xhr.open('GET', xhr.top + '?ls' + uq, true); xhr.setRequestHeader('Fnugg', '' + xhr.ts); xhr.onload = xhr.onerror = recvls; xhr.send(); r.nvis = r.lim; r.sb_msg = false; r.nextdir = xhr.top; clearTimeout(mpl.t_eplay); enspin('t'); enspin('f'); window.removeEventListener('scroll', r.tscroll); } function treegrow(e) { ev(e); if (this.textContent == '-') { while (this.nextSibling.nextSibling) { var rm = this.nextSibling.nextSibling; rm.parentNode.removeChild(rm); } this.textContent = '+'; onresize(); return; } var dst = this.getAttribute('dst'); get_tree('.', dst); } function recvls() { if (!xhrchk(this, L.fl_xe1, L.fl_xe2)) return; rtt = Date.now() - this.ts; r.nextdir = null; var cdir = get_evpath(), lfiles = ebi('files'), cur = lfiles.getAttribute('ts'); if (cur && parseInt(cur) > this.ts) { console.log("reject ls"); return; } lfiles.setAttribute('ts', this.ts); try { var res = JSON.parse(this.responseText); Object.assign(res, res.cfg); res.cfg.k; } catch (ex) { if (r.ls_cb) { r.ls_cb = null; return toast.inf(10, L.mm_nof); } if (!this.hydrate) { location = this.top; return; } return toast.err(30, "bad ?ls reply;\nexpected json, got this:\n\n" + esc(this.responseText + '')); } if (r.chk_index_html(this.top, res)) return; if (this.ts != res.fnugg && res.fnugg != 'nei' && sread('no_fnugg') !== '1') toast.warn(60, "WARNING: A proxy/CDN between your webbrowser and the server is misbehaving, and caching responses it shouldn't. As a result, you are now seeing stale directory listings. There will be many issues.\n\nIf you need to ignore this and stop these messages, you can set the global-option 'no-fnugg' on the server, or click π and run this: STG.no_fnugg=1"); for (var a = 0; a < res.files.length; a++) if (res.files[a].tags === undefined) res.files[a].tags = {}; sb_lg = res.sb_lg; sb_md = res.sb_md; dnsort = res.dnsort; read_dsort(res.dsort); dcrop = res.dcrop; dth3x = res.dth3x; dk = res.dk; dlni = res.dlni; if (!sread('dlni')) clmod(ebi('dlni'), 'on', treectl.dlni = dlni); dgrid = res.dgrid; if (!sread('griden')) clmod(ebi('griden'), 'on', thegrid.en = dgrid); srvinf = res.srvinf; if (rtt !== null) srvinf += (srvinf ? ' // rtt: ' : 'rtt: ') + rtt; var o = ebi('srv_info2'); if (o) o.innerHTML = ebi('srv_info').innerHTML = '' + srvinf + ''; if (res.ufavico && (!favico.en || !ebi('icot').value)) { while (qsr('head>link[rel~="icon"]')) { } document.head.insertAdjacentHTML('beforeend', res.ufavico); } if (this.hpush && !showfile.active()) hist_push(this.top + (dk ? '?k=' + dk : '')); if (!this.back) { var dirs = []; for (var a = 0; a < res.dirs.length; a++) { var dh = res.dirs[a].href, dn = dh.split('/')[0].split('?')[0], m = /[?&](k=[^&#]+)/.exec(dh); if (m) dn += '?' + m[1]; dirs.push(dn); } r.rendertree({ "a": dirs }, this.ts, ".", get_evpath() + (dk ? '?k=' + dk : '')); if (res.unlist) r.prunetree(res); } r.gentab(this.top, res); qsr('#dlt_t'); qsr('#dlt_f'); var lg0 = res.logues ? res.logues[0] || "" : "", lg1 = res.logues ? res.logues[1] || "" : "", mds = res.readmes && treectl.ireadme, md0 = mds ? res.readmes[0] || "" : "", md1 = mds ? res.readmes[1] || "" : "", dirchg = get_evpath() != cdir; if (lg1 === Ls.eng.f_empty) lg1 = L.f_empty; sandbox(ebi('pro'), sb_lg, sba_lg,'', lg0); if (dirchg) sandbox(ebi('epi'), sb_lg, sba_lg, '', lg1); clmod(ebi('pro'), 'mdo'); clmod(ebi('epi'), 'mdo'); if (md0) show_readme(md0, 0); if (md1) show_readme(md1, 1); else if (!dirchg) sandbox(ebi('epi'), sb_lg, sba_lg, '', lg1); if (this.hpush && !this.back) { var ofs = ebi('wrap').offsetTop; if (document.documentElement.scrollTop > ofs) document.documentElement.scrollTop = ofs; } wintitle(); var fun = r.ls_cb; if (fun) { r.ls_cb = null; fun(); } if (can_shr && in_shr && QS('#op_unpost.act')) goto('unpost'); } r.chk_index_html = function (top, res) { if (!r.idxh || !res || !res.files || noih) return; for (var a = 0; a < res.files.length; a++) if (/^index.html?(\?|$)/i.exec(res.files[a].href)) { location = vjoin(top, res.files[a].href); return true; } }; r.gentab = function (top, res) { showfile.untail(); var nodes = res.dirs.concat(res.files), html = mk_files_header(res.taglist), sel = msel.hist[top], ae = document.activeElement, cid = null, plain = [], seen = {}; in_shr = have_shr && top.startsWith(SR + have_shr); if (ae && /^tr$/i.exec(ae.nodeName)) if (ae = ae.querySelector('a[id]')) cid = ae.getAttribute('id'); var m = /[?&]k=([^&]+)/.exec(location.search); if (m) memo_dk(top, m[1]); r.lsc = res; if (res.unlist && !r.dots) { var ptn = new RegExp(res.unlist); for (var a = nodes.length - 1; a >= 0; a--) if (ptn.exec(uricom_dec(nodes[a].href.split('?')[0]))) nodes.splice(a, 1); } nodes = sortfiles(nodes); window.removeEventListener('scroll', r.tscroll); r.trunc = nodes.length > r.nvis && location.hash.length < 2; if (r.trunc) { for (var a = r.lim; a < nodes.length; a++) { var tn = nodes[a], tns = Object.keys(tn.tags || {}); plain.push(uricom_dec(tn.href.split('?')[0])); for (var b = 0; b < tns.length; b++) if (has(res.taglist, tns[b])) plain.push(tn.tags[tns[b]]); } nodes = nodes.slice(0, r.nvis); } showfile.files = []; html.push(''); for (var a = 0; a < nodes.length; a++) { var tn = nodes[a], bhref = tn.href.split('?')[0], fname = uricom_dec(bhref), hname = esc(fname), id = 'f-' + ('00000000' + crc32(fname)).slice(-8), lang = showfile.getlang(fname); while (seen[id]) // ejyefs ev69gg y9j8sg .opus id += 'a'; seen[id] = 1; if (lang) { showfile.files.push({ 'id': id, 'name': fname }); if (lang == 'md') tn.href = addq(tn.href, 'v'); } if (tn.lead == '-') tn.lead = '-txt-'; var cl = /\.PARTIAL$/.exec(fname) ? ' class="fade"' : '', ln = ['' + tn.lead + '' + hname + '' + filesizefun(tn.sz)]; for (var b = 0; b < res.taglist.length; b++) { var k = esc(res.taglist[b]), v = (tn.tags || {})[k] || "", sv = null; if (k == ".dur") sv = v ? s2ms(v) : ""; else if (k == ".up_at") sv = v ? unix2ui(v) : ""; else { ln.push(esc('' + v)); continue; } ln[ln.length - 1] += '' + sv; } ln = ln.concat([tn.ext, unix2ui(tn.ts)]).join(''); html.push(ln + ''); } html.push(''); html = html.join('\n'); set_files_html(html); if (r.dlni) { var o = QSA('#files a[id]'); for (var a = 0, aa = o.length; a < aa; a++) o[a].setAttribute('download', ''); } if (r.trunc) { r.setlazy(plain); if (!r.ask) { window.addEventListener('scroll', r.tscroll); setTimeout(r.tscroll, 100); } } else ebi('lazy').innerHTML = ''; function asdf() { showfile.mktree(); mukey.render(); reload_tree(); reload_browser(); tree_scrollto(); if (res.cfg) { acct = res.acct; have_up2k_idx = res.idx; have_tags_idx = res.itag; lifetime = res.lifetime; apply_perms(res); fileman.render(); } msel.loadsel(top, sel); if (cid) try { ebi(cid).closest('tr').focus(); } catch (ex) { } setTimeout(eval_hash, 1); } var m = scan_hash(hash0), url = null; if (m) { url = ebi(m[1]); if (url) { url = url.href; var mt = m[0] == 'a' ? 'audio' : /\.(webm|mkv)($|\?)/i.exec(url) ? 'video' : 'image' if (mt == 'image') { url = addq(url, 'cache'); console.log(url); new Image().src = url; } } } if (url) setTimeout(asdf, 1); else asdf(); } r.hydrate = function () { qsr('#bbsw'); srvinf = ebi('srv_info').innerHTML.slice(6, -7); if (ls0 === null) { r.ls_cb = showfile.addlinks; return setck('js=y', function () { r.reqls(get_evpath(), false, undefined, true); }); } ls0.unlist = unlist0; ls0.u2ts = u2ts; var top = get_evpath(); if (r.chk_index_html(top, ls0)) return; r.gentab(top, ls0); pbar.onresize(); vbar.onresize(); showfile.addlinks(); setTimeout(eval_hash, 1); }; function memo_dk(vp, k) { dks[vp] = k; var lv = vp + "?" + k; if (has(ldks, lv)) return; ldks.unshift(lv); if (ldks.length > 32) { var keep = [], evp = get_evpath(); for (var a = 0; a < ldks.length; a++) { var s = ldks[a]; if (evp.startsWith(s.replace(/\?[^?]+$/, ''))) keep.push(s); } var lim = 32 - keep.length; for (var a = 0; a < lim; a++) { if (!has(keep, ldks[a])) keep.push(ldks[a]) } ldks = keep; } jwrite('dks', ldks); } r.setlazy = function (plain) { var html = ['
          ', esc(plain.join(' ')), '
          '], all = r.lsc.files.length + r.lsc.dirs.length, nxt = r.nvis * 4; if (r.ask) html.push((nxt >= all ? L.fbd_all : L.fbd_more).format(r.nvis, all, nxt)); ebi('lazy').innerHTML = html.join('\n'); try { ebi('bd_all').onclick = function (e) { ev(e); r.showmore(all); }; ebi('bd_more').onclick = function (e) { ev(e); r.showmore(nxt); }; } catch (ex) { } }; r.showmore = function (n, cb) { window.removeEventListener('scroll', r.tscroll); console.log('nvis {0} -> {1}'.format(r.nvis, n)); r.nvis = n; ebi('lazy').innerHTML = ''; ebi('wrap').style.opacity = 0.4; document.documentElement.scrollLeft = 0; setTimeout(function () { r.gentab(get_evpath(), r.lsc); ebi('wrap').style.opacity = CLOSEST ? 'unset' : 1; if (cb) cb(); }, 1); }; r.tscroll = function () { var el = r.trunc ? ebi('plazy') : null; if (!el || ebi('lazy').style.display || ebi('unsearch')) return; var sy = yscroll() + window.innerHeight, ty = el.offsetTop; if (sy <= ty) return; window.removeEventListener('scroll', r.tscroll); var all = r.lsc.files.length + r.lsc.dirs.length; if (r.nvis * 16 <= all) { console.log("{0} ({1} * 16) <= {2}".format(r.nvis * 16, r.nvis, all)); r.showmore(r.nvis * 4); } else { console.log("{0} ({1} * 16) > {2}".format(r.nvis * 16, r.nvis, all)); r.showmore(all); } }; function parsetree(res, top) { var ret = ''; for (var a = 0; a < res.a.length; a++) { if (res.a[a] !== '') res['k' + res.a[a]] = 0; } delete res['a']; var keys = Object.keys(res); for (var a = 0; a < keys.length; a++) keys[a] = [uricom_dec(keys[a]), keys[a]]; if (ENATSORT) keys.sort(function (a, b) { return NATSORT.compare(a[0], b[0]); }); else keys.sort(function (a, b) { return a[0].localeCompare(b[0]); }); for (var a = 0; a < keys.length; a++) { var kk = keys[a][1], m = /(\?k=[^\n]+)/.exec(kk), kdk = m ? m[1] : '', ks = kk.replace(kdk, '').slice(1), ded = ks.endsWith('\n'), k = uricom_sdec(ded ? ks.replace(/\n$/, '') : ks), hek = esc(k[0]), uek = k[1] ? uricom_enc(k[0], true) : k[0], url = '/' + (top ? top + uek : uek) + '/', sym = res[kk] ? '-' : '+', link = '' + sym + '' + hek + ''; if (res[kk]) { var subtree = parsetree(res[kk], url.slice(1)); ret += '
        • ' + link + '\n
            \n' + subtree + '
        • \n'; } else { ret += (ded ? '
        • ' : '
        • ') + link + '
        • \n'; } } return ret; } function scaletree(e) { ev(e); treesz += parseInt(this.getAttribute("step")); if (!isNum(treesz)) treesz = 16; treesz = clamp(treesz, 2, 120); swrite('treesz', treesz); onresize(); } ebi('entree').onclick = r.entree; ebi('detree').onclick = r.detree; ebi('visdir').onclick = tree_scrollto; ebi('twig').onclick = scaletree; ebi('twobytwo').onclick = scaletree; var cs = sread('entreed'), vw = window.innerWidth / parseFloat(getComputedStyle(document.body)['font-size']); if (notree) { cs = 'na'; r.detree(null, 1); } if (cs == 'tree' || (cs != 'na' && vw >= 60)) r.entree(null, true); r.onpopfun = function (e) { console.log("h-pop " + e.state); if (!e.state) return; var url = new URL(e.state, "https://" + location.host), req = url.pathname, hbase = req, cbase = location.pathname, mdoc = /[?&]doc=/.exec('' + url), mdk = /[?&](k=[^&#]+)/.exec('' + url); if (mdoc && hbase == cbase) return showfile.show(hbase + showfile.sname(url.search), true); if (mdk) req += '?' + mdk[1]; r.goto(req, false, true); }; var evp = get_evpath() + (dk ? '?k=' + dk : ''); hist_replace(evp + location.hash); r.onscroll = onscroll; return r; })(); function enspin(i) { i = 'dlt_' + i; if (ebi(i)) return; var d = mknod('div', i, SPINNER); d.className = 'dumb_loader_thing'; if (SPINNER_CSS) d.style.cssText = SPINNER_CSS; document.body.appendChild(d); } var wfp_debounce = (function () { var r = { 'n': 0, 't': 0 }; r.hide = function () { if (!sb_lg && !sb_md) return; if (++r.n <= 1) { r.n = 1; clearTimeout(r.t); r.t = setTimeout(r.reset, 300); ebi('wfp').style.opacity = 0.1; } }; r.show = function () { if (!sb_lg && !sb_md) return; if (--r.n <= 0) { r.n = 0; clearTimeout(r.t); ebi('wfp').style.opacity = CLOSEST ? 'unset' : 1; } }; r.reset = function () { r.n = 0; r.show(); }; return r; })(); function apply_perms(res) { perms = res.perms || []; var axs = [], aclass = '>', chk = ['read', 'write', 'move', 'delete', 'get', 'admin']; if (konmai < 0) { acct = 'Ted Faro'; srvinf = 'FAS Nexus
          // 57.3 EiB free of 127 EiB'; res.shr_who = 'auth'; perms = res.perms = chk; have_up2k_idx = have_tags_idx = 1; have_mv = have_del = true; } var a = QS('#ops a[data-dest="up2k"]'); if (have_up2k_idx) { a.removeAttribute('data-perm'); a.setAttribute('tt', L.ot_u2i); } else { a.setAttribute('data-perm', 'write'); a.setAttribute('tt', L.ot_u2w); } clmod(ebi('srch_form'), 'tags', have_tags_idx); a.style.display = ''; tt.att(QS('#ops')); for (var a = 0; a < chk.length; a++) if (has(perms, chk[a])) axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1)); axs = axs.join('-'); if (perms.length == 1) { aclass = ' class="warn">'; axs += '-Only'; } var dst = "?h"; if (idp_login && acct == "*") dst = idp_login.replace(/\{dst\}/g, get_evpath()); ebi('acc_info').innerHTML = '' + srvinf + '' + (acct != '*' ? '
          ' : '' + L.login + ''); var o = QSA('#ops>a[data-perm]'); for (var a = 0; a < o.length; a++) { var display = ''; var needed = o[a].getAttribute('data-perm').split(' '); for (var b = 0; b < needed.length; b++) { if (!has(perms, needed[b])) { display = 'none'; } } o[a].style.display = display; } var o = QSA('#ops>a[data-dep], #u2conf td[data-dep]'); for (var a = 0; a < o.length; a++) o[a].style.display = ( o[a].getAttribute('data-dep') != 'idx' || have_up2k_idx ) ? '' : 'none'; if (in_shr) ebi('opa_srch').style.display = 'none'; var act = QS('#ops>a.act'); if (act && act.style.display === 'none') goto(); document.body.setAttribute('perms', perms.join(' ')); var have_write = has(perms, "write"), have_read = has(perms, "read"), de = document.documentElement, tds = QSA('#u2conf td'); shr_who = res.shr_who || shr_who; can_shr = acct != '*' && (have_read || have_write) && ( (shr_who == 'a' && has(perms, 'admin')) || (shr_who == 'auth')); clmod(de, "read", have_read); clmod(de, "write", have_write); clmod(de, "nread", !have_read); clmod(de, "nwrite", !have_write); for (var a = 0; a < tds.length; a++) { tds[a].style.display = (have_write || tds[a].getAttribute('data-perm') == 'read') ? 'table-cell' : 'none'; } if (res.frand) ebi('u2rand').parentNode.style.display = 'none'; u2ts = res.u2ts; if (up2k) up2k.set_fsearch(); if (res.cfg) rw_edit = res.rw_edit; enre_rw_edit(); ebi('new_mdi').innerHTML = has(perms, "delete") ? L.nmd_i1 : L.nmd_i2.format(rw_edit.replace(/,/g, '/')); widget.setvis(); thegrid.setvis(); if (!have_read && have_write) goto('up2k'); } function tr2id(tr) { try { return tr.cells[1].querySelector('a[id]').getAttribute('id'); } catch (ex) { return null; } } function find_file_col(txt) { var i = -1, min = false, tds = ebi('files').tHead.getElementsByTagName('th'); for (var a = 0; a < tds.length; a++) { var spans = tds[a].getElementsByTagName('span'); if (spans.length && spans[0].textContent == txt) { min = (tds[a].className || '').indexOf('min') !== -1; i = a; break; } } if (i == -1) return; return [i, min]; } function mk_files_header(taglist) { var html = [ '', 'c', 'File Name', 'Size' ]; for (var a = 0; a < taglist.length; a++) { var tag = taglist[a], c1 = tag.slice(0, 1).toUpperCase(); tag = esc(c1 + tag.slice(1)); if (c1 == '.') tag = '' + tag.slice(1); else tag = '' + tag; html.push(tag + ''); } html = html.concat([ 'T', 'Date', '', ]); return html; } var filecols = (function () { var r = { 'picking': false }; var hidden = jread('filecols', []); r.add_btns = function () { var ths = QSA('#files>thead th>span'); for (var a = 0, aa = ths.length; a < aa; a++) { var th = ths[a].parentElement, toh = ths[a].outerHTML, // !ff10 ttv = L.cols[ths[a].textContent]; ttv = (ttv ? ttv + '; ' : '') + 'id=' + th.getAttribute('name') + ''; if (!MOBILE && toh) { th.innerHTML = '' + toh; th.getElementsByTagName('a')[0].onclick = ev_row_tgl; } if (ttv) { th.setAttribute("tt", ttv); th.setAttribute("ttd", "u"); th.setAttribute("ttm", "12"); } } }; function hcols_click(e) { ev(e); var t = e.target; if (t.tagName != 'A') return; r.toggle(t.textContent); } r.uivis = function () { var hcols = ebi('hcols'); hcols.previousSibling.style.display = hcols.style.display = ((!thegrid || !thegrid.en) && (hidden.length || MOBILE)) ? 'block' : 'none'; }; r.set_style = function (unhide) { hidden.sort(); if (!unhide) unhide = []; var html = [], hcols = ebi('hcols'); for (var a = 0; a < hidden.length; a++) { var ttv = L.cols[hidden[a]], tta = ttv ? ' tt="' + ttv + '">' : '>'; html.push(''); } hcols.innerHTML = html.join('\n'); hcols.onclick = hcols_click; r.uivis(); r.add_btns(); var ohidden = [], ths = QSA('#files>thead th'), ncols = ths.length; for (var a = 0; a < ncols; a++) { var span = ths[a].getElementsByTagName('span'); if (span.length <= 0) continue; var name = span[0].textContent, cls = false; if (has(hidden, name) && !has(unhide, name)) { ohidden.push(a); cls = true; } clmod(ths[a], 'min', cls) } for (var a = 0; a < ncols; a++) { var cls = has(ohidden, a) ? 'min' : '', tds = QSA('#files>tbody>tr>td:nth-child(' + (a + 1) + ')'); for (var b = 0, bb = tds.length; b < bb; b++) tds[b].className = cls; } if (tt) { tt.att(ebi('hcols')); tt.att(QS('#files>thead')); } }; r.setvis = function (name, vis) { var ofs = hidden.indexOf(name); if (ofs !== -1 && vis != 0) hidden.splice(ofs, 1); else if (vis != 1) { if (!sread("chide_ok")) { return modal.confirm(L.f_chide.format(name), function () { swrite("chide_ok", 1); r.toggle(name); }, null); } hidden.push(name); } jwrite("filecols", hidden); r.set_style(); }; r.show = function (name) { r.setvis(name, 1); }; r.hide = function (name) { r.setvis(name, 0); }; r.toggle = function (name) { r.setvis(name, -1); }; ebi('hcolsr').onclick = function (e) { ev(e); r.reset(true); }; if (MOBILE) ebi('hcolsh').onclick = function (e) { ev(e); if (r.picking) return r.unpick(); var lbs = QSA('#files>thead th'); for (var a = 0; a < lbs.length; a++) { lbs[a].onclick = function (e) { ev(e); if (toast.tag == 'pickhide') toast.hide(); r.hide(e.target.textContent); }; }; r.picking = true; clmod(ebi('files'), 'hhpick', 1); toast.inf(0, L.cl_hpick, 'pickhide'); }; r.unpick = function () { r.picking = false; toast.inf(5, L.cl_hcancel); clmod(ebi('files'), 'hhpick'); var lbs = QSA('#files>thead th'); for (var a = 0; a < lbs.length; a++) lbs[a].onclick = null; }; r.reset = function (force) { if (force || JSON.stringify(def_hcols) != sread('hfilecols')) { console.log("applying default hidden-cols"); hidden = []; jwrite('hfilecols', def_hcols); for (var a = 0; a < def_hcols.length; a++) { var t = def_hcols[a]; t = t.slice(0, 1).toUpperCase() + t.slice(1); if (t.startsWith(".")) t = t.slice(1); if (hidden.indexOf(t) == -1) hidden.push(t); } jwrite("filecols", hidden); } r.set_style(); } r.reset(); try { var ci = find_file_col('dur'), i = ci[0], rows = ebi('files').tBodies[0].rows; for (var a = 0, aa = rows.length; a < aa; a++) { var c = rows[a].cells[i]; if (c && c.textContent) c.textContent = s2ms(c.textContent); } } catch (ex) { } return r; })(); var mukey = (function () { var maps = { "rekobo_alnum": [ "1B ", "2B ", "3B ", "4B ", "5B ", "6B ", "7B ", "8B ", "9B ", "10B", "11B", "12B", "1A ", "2A ", "3A ", "4A ", "5A ", "6A ", "7A ", "8A ", "9A ", "10A", "11A", "12A" ], "rekobo_classic": [ "B ", "F# ", "Db ", "Ab ", "Eb ", "Bb ", "F ", "C ", "G ", "D ", "A ", "E ", "Abm", "Ebm", "Bbm", "Fm ", "Cm ", "Gm ", "Dm ", "Am ", "Em ", "Bm ", "F#m", "Dbm" ], "traktor_musical": [ "B ", "Gb ", "Db ", "Ab ", "Eb ", "Bb ", "F ", "C ", "G ", "D ", "A ", "E ", "Abm", "Ebm", "Bbm", "Fm ", "Cm ", "Gm ", "Dm ", "Am ", "Em ", "Bm ", "Gbm", "Dbm" ], "traktor_sharps": [ "B ", "F# ", "C# ", "G# ", "D# ", "A# ", "F ", "C ", "G ", "D ", "A ", "E ", "G#m", "D#m", "A#m", "Fm ", "Cm ", "Gm ", "Dm ", "Am ", "Em ", "Bm ", "F#m", "C#m" ], "traktor_open": [ "6d ", "7d ", "8d ", "9d ", "10d", "11d", "12d", "1d ", "2d ", "3d ", "4d ", "5d ", "6m ", "7m ", "8m ", "9m ", "10m", "11m", "12m", "1m ", "2m ", "3m ", "4m ", "5m " ] }, defnot = 'rekobo_alnum'; var map = {}, html = [], cb = ebi('key_notation'); for (var k in maps) { if (!maps.hasOwnProperty(k)) continue; html.push(''.format(k)); for (var a = 0; a < 24; a++) maps[k][a] = maps[k][a].trim(); } cb.innerHTML = html.join(''); function set_key_notation() { load_notation(cb.value); try_render(); } function load_notation(notation) { swrite("cpp_keynot", notation); map = {}; var dst = maps[notation]; for (var k in maps) if (k != notation && maps.hasOwnProperty(k)) for (var a = 0; a < 24; a++) if (maps[k][a] != dst[a]) map[maps[k][a]] = dst[a]; } function render() { var ci = find_file_col('Key'); if (!ci) return; var i = ci[0], min = ci[1], rows = ebi('files').tBodies[0].rows; if (min) for (var a = 0, aa = rows.length; a < aa; a++) { var c = rows[a].cells[i]; if (!c) continue; var v = c.getAttribute('html'); c.setAttribute('html', map[v] || v); } else for (var a = 0, aa = rows.length; a < aa; a++) { var c = rows[a].cells[i]; if (!c) continue; var v = c.textContent; c.textContent = map[v] || v; } } function try_render() { try { render(); } catch (ex) { console.log("key notation failed: " + ex); } } var notation = sread("cpp_keynot") || defnot; if (!maps[notation]) notation = defnot; cb.value = notation; cb.onchange = set_key_notation; load_notation(notation); return { "render": try_render }; })(); var light, theme, themen; var settheme = (function () { var r = {}, ax = 'abcdefghijklmnopqrstuvwx', tre = '🌲', chldr = !SPINNER_CSS && SPINNER == tre; r.ldr = { '4':['🌴'], '5':['🌭', 'padding:0 0 .7em .7em;filter:saturate(3)'], '6':['📞', 'padding:0;filter:brightness(2) sepia(1) saturate(3) hue-rotate(60deg)'], '7':['▲', 'font-size:3em'], //cp437 }; theme = sread('cpp_thm') || 'a'; if (!/^[a-x][yz]/.exec(theme)) theme = dtheme; themen = theme.split(/ /)[0]; light = !!(theme.indexOf('y') + 1); function freshen() { var cl = document.documentElement.className; cl = cl.replace(/\b(light|dark|[a-z]{1,2})\b/g, '').replace(/ +/g, ' '); document.documentElement.className = cl + ' ' + theme + ' '; pbar.drawbuf(); pbar.drawpos(); vbar.draw(); showfile.setstyle(); bchrome(); var html = [], cb = ebi('themes'), itheme = ax.indexOf(theme[0]) * 2 + (light ? 1 : 0), names = ['classic dark', 'classic light', 'pm-monokai', 'flat light', 'vice', 'hotdog stand', 'hacker', 'hi-con', 'phi95 dark', 'phi95']; for (var a = 0; a < themes; a++) html.push(''.format(a, names[a] || 'custom')); ebi('themes').innerHTML = html.join(''); cb.value = itheme; cb.onchange = r.onsel; if (chldr) { var x = r.ldr[itheme] || [tre]; SPINNER = x[0]; SPINNER_CSS = x[1]; } bcfg_set('light', light); } r.onsel = function () { r.go(parseInt(ebi('themes').value)); }; r.go = function (i) { light = i % 2 == 1; var c = ax[Math.floor(i / 2)], l = light ? 'y' : 'z'; theme = c + l + ' ' + c + ' ' + l; themen = c + l; swrite('cpp_thm', theme); freshen(); }; var m = /[?&]theme=([0-9]+)/.exec(sloc0); if (m) r.go(parseInt(m[1])); else freshen(); return r; })(); var setfszf = (function () { function freshen() { var cb = ebi('fszfmt'), fmt = sread("fszfmt", humansize_fmts) || window.dfszf; if (!has(humansize_fmts, fmt)) fmt = '1'; window.filesizefun = window['humansize_' + fmt]; cb.onchange = onch; if (cb.value != fmt) cb.value = fmt; } function onch(e) { ev(e); setfmt(ebi('fszfmt').value) } function setfmt(fmt) { swrite("fszfmt", fmt); freshen(); treectl.gentab(get_evpath(), treectl.lsc); } freshen(); return setfmt; })(); (function () { function freshen() { var cb = ebi('langs'), html = []; for (var a = 0; a < LANGN.length; a++) { html.push(''.format(LANGN[a][0], LANGN[a][1])); } cb.innerHTML = html.join(''); cb.onchange = setlang; cb.value = lang; } function setlang(e) { ev(e); lang = ebi('langs').value; setck('cplng=' + lang); freshen(); var t = L.tt == 'English' ? '' : Ls.eng.lang_set; modal.confirm(L.lang_set + "\n\n" + t, location.reload.bind(location), null); } freshen(); })(); var arcfmt = (function () { if (!ebi('arc_fmt')) return { "render": function () { } }; var html = [], fmts = [ ["tar", "tar", L.fz_tar], ["pax", "tar=pax", L.fz_pax], ["tgz", "tar=gz", L.fz_targz], ["txz", "tar=xz", L.fz_tarxz], ["zip", "zip", L.fz_zip8], ["zip_dos", "zip=dos", L.fz_zipd], ["zip_crc", "zip=crc", L.fz_zipc] ]; for (var a = 0; a < fmts.length; a++) { var k = fmts[a][0]; html.push( '' + ''); } ebi('arc_fmt').innerHTML = html.join('\n'); var fmt = sread("arc_fmt"); if (!ebi('arcfmt_' + fmt)) fmt = "zip"; ebi('arcfmt_' + fmt).checked = true; function render() { var arg = null, tds = QSA('#files tbody td:first-child a'); for (var a = 0; a < fmts.length; a++) if (fmts[a][0] == fmt) arg = fmts[a][1]; for (var a = 0, aa = tds.length; a < aa; a++) { var o = tds[a], txt = o.textContent, href = o.getAttribute('href'); if (!/^(zip|tar|pax|tgz|txz)$/.exec(txt)) continue; var m = /(.*[?&])(tar|zip)([^&#]*)(.*)$/.exec(href); if (!m) throw new Error('missing arg in url'); o.setAttribute("href", m[1] + arg + m[4]); o.textContent = fmt.split('_')[0]; } ebi('selzip').textContent = fmt.split('_')[0]; ebi('selzip').setAttribute('fmt', arg); QS('#zip1 span').textContent = fmt.split('_')[0]; ebi('zip1').setAttribute("href", get_evpath() + (dk ? '?k=' + dk + '&': '?') + arg); if (!have_zip) { ebi('zip1').style.display = 'none'; ebi('selzip').style.display = 'none'; } } function try_render() { try { render(); } catch (ex) { console.log("arcfmt failed: " + ex); } } function change_fmt(e) { ev(e); fmt = this.getAttribute('value'); swrite("arc_fmt", fmt); try_render(); } var o = QSA('#arc_fmt input'); for (var a = 0; a < o.length; a++) { o[a].onchange = change_fmt; } return { "render": try_render }; })(); var msel = (function () { var r = {}; r.sel = null; r.all = null; r.hist = {}; r.so = null; // selection origin r.pr = null; // previous range r.load = function (reset) { if (r.sel && !reset) return; r.sel = []; if (r.all && r.all.length) { for (var a = 0; a < r.all.length; a++) { var ao = r.all[a]; ao.sel = clgot(ebi(ao.id).closest('tr'), 'sel'); if (ao.sel) r.sel.push(ao); } if (!reset) return; } r.all = []; var links = QSA('#files tbody td:nth-child(2) a:last-child'), is_srch = !!ebi('unsearch'), vbase = get_evpath(); for (var a = 0, aa = links.length; a < aa; a++) { var qhref = links[a].getAttribute('href'), href = qhref.split('?')[0], item = {}; if (href.endsWith('/')) { href = href.slice(0, -1); item.isd = true; } item.id = links[a].getAttribute('id'); item.sel = clgot(links[a].closest('tr'), 'sel'); item.vp = href.indexOf('/') !== -1 ? href : vbase + href; if (dk) { var m = /[?&](k=[^&#]+)/.exec(qhref); item.q = m ? '?' + m[1] : ''; } else item.q = ''; r.all.push(item); if (item.sel) r.sel.push(item); if (!is_srch) links[a].closest('tr').setAttribute('tabindex', '0'); } }; r.loadsel = function (vp, sel) { if (!sel || !r.so || !ebi(r.so)) r.so = r.pr = null; if (!sel) return r.origin_id(null); r.hist[vp] = sel; r.sel = []; r.load(); var vsel = new Set(); for (var a = 0; a < sel.length; a++) vsel.add(sel[a].vp); for (var a = 0; a < r.all.length; a++) if (vsel.has(r.all[a].vp)) clmod(ebi(r.all[a].id).closest('tr'), 'sel', 1); r.selui(); }; r.getsel = function () { r.load(); return r.sel; }; r.getall = function () { r.load(); return r.all; }; r.selui = function (reset) { r.sel = null; if (reset) r.all = null; clmod(ebi('wtoggle'), 'sel', r.getsel().length); thegrid.loadsel(); fileman.render(); showfile.updtree(); if (r.sel.length) r.hist[get_evpath()] = r.sel; else delete r.hist[get_evpath()]; }; r.seltgl = function (e) { ev(e); var tr = this.parentNode, id = tr2id(tr); if ((treectl.csel || !thegrid.en || thegrid.sel) && e.shiftKey && r.so && id && r.so != id) { var o1 = -1, o2 = -1; for (a = 0; a < r.all.length; a++) { var ai = r.all[a].id; if (ai == r.so) o1 = a; if (ai == id) o2 = a; } var st = r.all[o1].sel; if (o1 > o2) o2 = [o1, o1 = o2][0]; if (r.pr) { // invert previous range, in case it was narrowed for (var a = r.pr[0]; a <= r.pr[1]; a++) clmod(ebi(r.all[a].id).closest('tr'), 'sel', !st); // and invert current selection if repeated if (r.pr[0] === o1 && r.pr[1] === o2) st = !st; } for (var a = o1; a <= o2; a++) clmod(ebi(r.all[a].id).closest('tr'), 'sel', st); r.pr = [o1, o2]; if (window.getSelection) window.getSelection().removeAllRanges(); } else { clmod(tr, 'sel', 't'); r.origin_tr(tr); } r.selui(); }; r.origin_tr = function (tr) { r.so = tr2id(tr); r.pr = null; }; r.origin_id = function (id) { r.so = id; r.pr = null; }; r.evsel = function (e, fun) { ev(e); r.so = r.pr = null; var trs = QSA('#files tbody tr'); for (var a = 0, aa = trs.length; a < aa; a++) clmod(trs[a], 'sel', fun); r.selui(); } ebi('selall').onclick = function (e) { r.evsel(e, "add"); }; ebi('selinv').onclick = function (e) { r.evsel(e, "t"); }; ebi('selzip').onclick = function (e) { ev(e); var sel = r.getsel(), arg = ebi('selzip').getAttribute('fmt'), frm = mknod('form'), txt = []; if (dk) arg += '&k=' + dk; for (var a = 0; a < sel.length; a++) txt.push(vsplit(sel[a].vp)[1]); txt = txt.join('\n'); frm.setAttribute('action', '?' + arg); frm.setAttribute('method', 'post'); frm.setAttribute('target', '_blank'); frm.setAttribute('enctype', 'multipart/form-data'); frm.innerHTML = '' + ''; frm.style.display = 'none'; qsr('#widgeti>form'); ebi('widgeti').appendChild(frm); var obj = ebi('ziptxt'); obj.value = txt; console.log(txt); frm.submit(); }; ebi('seldl').onclick = function (e) { ev(e); var sel = r.getsel(); for (var a = 0; a < sel.length; a++) if (sel[a].isd) toast.warn(7, L.f_dl_nd + esc(sel[a].vp)); else dl_file(sel[a].vp + sel[a].q); }; r.render = function () { var tds = QSA('#files tbody td+td+td'), is_srch = !!ebi('unsearch'); if (!is_srch) for (var a = 0, aa = tds.length; a < aa; a++) tds[a].onclick = r.seltgl; r.selui(true); arcfmt.render(); fileman.render(); var zipvis = (is_srch || !have_zip) ? 'none' : ''; ebi('selzip').style.display = zipvis; ebi('zip1').style.display = zipvis; } return r; })(); (function () { if (!FormData) return; var form = QS('#op_new_md>form'), tb = QS('#op_new_md input[name="name"]'); form.onsubmit = function (e) { if (!has(perms, "delete") && !re_rw_edit.test(tb.value)) { ev(e); toast.err(10, L.nmd_i2.format(rw_edit.replace(/,/g, '/'))); return false; } if (tb.value) { if (toast.tag == L.mk_noname) toast.hide(); return true; } ev(e); toast.err(10, L.mk_noname, L.mk_noname); return false; }; })(); (function () { if (!FormData) return; var form = QS('#op_mkdir>form'), tb = QS('#op_mkdir input[name="name"]'), sf = mknod('div'); clmod(sf, 'msg', 1); form.parentNode.appendChild(sf); form.onsubmit = function (e) { ev(e); var dn = tb.value; if (!dn) { toast.err(10, L.mk_noname, L.mk_noname); return false; } if (toast.tag == L.mk_noname || toast.tag == L.fd_xe1) toast.hide(); clmod(sf, 'vis', 1); sf.textContent = 'creating "' + dn + '"...'; var fd = new FormData(); fd.append("act", "mkdir"); fd.append("name", dn); var xhr = new XHR(); xhr.vp = get_evpath(); xhr.dn = dn; xhr.open('POST', dn.startsWith('/') ? (SR || '/') : xhr.vp, true); xhr.onload = xhr.onerror = cb; xhr.responseType = 'text'; xhr.send(fd); return false; }; function cb() { if (this.vp !== get_evpath()) { sf.textContent = 'aborted due to location change'; return; } xhrchk(this, L.fd_xe1, L.fd_xe2); if (this.status !== 201) { sf.textContent = 'error: ' + hunpre(this.responseText); return; } tb.value = ''; clmod(sf, 'vis'); sf.textContent = ''; var dn = this.getResponseHeader('X-New-Dir'); dn = dn ? '/' + dn + '/' : uricom_enc(this.dn); treectl.goto(dn, true); tree_scrollto(); } })(); (function () { var form = QS('#op_msg>form'), tb = QS('#op_msg input[name="msg"]'), sf = mknod('div'); clmod(sf, 'msg', 1); form.parentNode.appendChild(sf); form.onsubmit = function (e) { ev(e); clmod(sf, 'vis', 1); sf.textContent = 'sending...'; var xhr = new XHR(), sel = msel.getsel(), msg = uricom_enc(tb.value), ct = 'application/x-www-form-urlencoded;charset=UTF-8'; for (var a = 0; a < sel.length; a++) msg += "&sel=" + sel[a].vp; xhr.msg = msg; xhr.open('POST', get_evpath(), true); xhr.responseType = 'text'; xhr.onload = xhr.onerror = cb; xhr.setRequestHeader('Content-Type', ct); if (xhr.overrideMimeType) xhr.overrideMimeType('Content-Type', ct); xhr.send('msg=' + xhr.msg); return false; }; function cb() { xhrchk(this, L.fsm_xe1, L.fsm_xe2); if (this.status < 200 || this.status > 202) { sf.textContent = 'error: ' + hunpre(this.responseText); return; } tb.value = ''; clmod(sf, 'vis'); var txt = 'sent: ' + esc(this.msg) + ''; if (this.status == 202) txt += '
            got: ' + esc(this.responseText) + ''; sf.innerHTML = txt; setTimeout(function () { treectl.goto(); }, 100); } })(); var globalcss = (function () { var ret = ''; return function () { if (ret) return ret; var dcs = document.styleSheets; for (var a = 0; a < dcs.length; a++) { var ds, base = ''; try { base = dcs[a].href; if (!base) continue; ds = dcs[a].cssRules; base = base.replace(/[^/]+$/, ''); for (var b = 0; b < ds.length; b++) { var css = ds[b].cssText.split(/\burl\(/g); ret += css[0]; for (var c = 1; c < css.length; c++) { var m = /(^ *["']?)(.*)/.exec(css[c]), delim = m[1], ctxt = m[2], is_abs = /^\/|[^)/:]+:\/\//.exec(ctxt); ret += 'url(' + delim + (is_abs ? '' : base) + ctxt; } ret += '\n'; } if (ret.indexOf('\n@import') + 1) { var c0 = ret.split('\n'), c1 = [], c2 = []; for (var a = 0; a < c0.length; a++) (c0[a].startsWith('@import') ? c1 : c2).push(c0[a]); ret = c1.concat(c2).join('\n'); } } catch (ex) { console.log('could not read css', a, base); } } return ret; }; })(); var sandboxjs = (function () { var ret = '', busy = false, url = SR + '/.cpr/w/util.js?_=' + TS, tag = ''; return function () { if (ret || busy) return ret || tag; var xhr = new XHR(); xhr.open('GET', url, true); xhr.onload = function () { if (this.status == 200) ret = ''; }; xhr.send(); busy = true; return tag; }; })(); function show_md(md, name, div, url, depth) { var errmsg = L.md_eshow + name + ':\n\n', now = get_evpath(); url = url || now; if (url != now) return; wfp_debounce.hide(); if (!marked) { if (depth) { clmod(div, 'raw', 1); div.textContent = "--[ " + name + " ]---------\r\n" + md; return toast.warn(10, errmsg + (WebAssembly ? 'failed to load marked.js' : 'your browser is too old')); } wfp_debounce.n--; return import_js(SR + '/.cpr/w/deps/marked.js', function () { show_md(md, name, div, url, 1); }); } md_plug = {} md = load_md_plug(md, 'pre'); md = load_md_plug(md, 'post', sb_md); var marked_opts = { headerPrefix: 'md-', breaks: !md_no_br, gfm: true }; var ext = md_plug.pre; if (ext) Object.assign(marked_opts, ext[0]); try { clmod(div, 'mdo', 1); var md_html = marked.parse(md, marked_opts); if (!have_emp) md_html = DOMPurify.sanitize(md_html); if (sandbox(div, sb_md, sba_md, 'mdo', md_html)) return; ext = md_plug.post; ext = ext ? [ext[0].render, ext[0].render2] : []; for (var a = 0; a < ext.length; a++) if (ext[a]) try { ext[a](div); } catch (ex) { console.log(ex); } var els = QSA('#epi a'); for (var a = 0, aa = els.length; a < aa; a++) { var href = els[a].getAttribute('href'); if (!href || !href.startsWith('#') || href.startsWith('#md-')) continue; els[a].setAttribute('href', '#md-' + href.slice(1)); } md_th_set(); set_tabindex(); var hash = location.hash; if (hash.startsWith('#md-')) setTimeout(function () { try { QS(hash).scrollIntoView(); } catch (ex) { } }, 1); } catch (ex) { toast.warn(10, errmsg + ex); } wfp_debounce.show(); } function set_tabindex() { var els = QSA('pre'); for (var a = 0, aa = els.length; a < aa; a++) els[a].setAttribute('tabindex', '0'); } function show_readme(md, n) { var tgt = ebi(n ? 'epi' : 'pro'); if (!treectl.ireadme) return sandbox(tgt, '', '', '', 'a'); show_md(md, n ? 'README.md' : 'PREADME.md', tgt); } for (var a = 0; a < readmes.length; a++) if (readmes[a]) show_readme(readmes[a], a); function sandbox(tgt, rules, allow, cls, html) { if (!treectl.ireadme) { tgt.innerHTML = html ? L.md_off : ''; return; } if (!rules || (html || '').indexOf('<') == -1) { tgt.innerHTML = html; clmod(tgt, 'sb'); return false; } if (!CLOSEST) { tgt.textContent = html; clmod(tgt, 'sb'); return false; } clmod(tgt, 'sb', 1); var tid = tgt.getAttribute('id'), hash = location.hash, want = ''; if (!cls) wfp_debounce.hide(); if (hash.startsWith('#md-')) want = hash.slice(1); var env = '', tags = QSA('script'); for (var a = 0; a < tags.length; a++) { var js = tags[a].innerHTML; if (js && js.indexOf('have_up2k_idx') + 1) env = js.split(/\blogues *=/)[0] + 'a;'; } html = '' + html + '' + sandboxjs() + ''; var fr = mknod('iframe'); fr.setAttribute('title', 'folder ' + tid + 'logue'); fr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : ''); fr.setAttribute('allow', allow); fr.setAttribute('srcdoc', html); tgt.appendChild(fr); treectl.sb_msg = true; return true; } window.addEventListener("message", function (e) { if (!treectl.sb_msg) return; try { console.log('msg:' + e.data); var t = e.data.split(/ /g); if (t[0] == 'iheight') { var el = QSA(t[1] + '>iframe'); el = el[el.length - 1]; if (wfp_debounce.n) while (el.previousSibling) el.parentNode.removeChild(el.previousSibling); el.style.height = (parseInt(t[2]) + SBH) + 'px'; el.style.visibility = CLOSEST ? 'unset' : 'block'; wfp_debounce.show(); } else if (t[0] == 'iscroll') { var y1 = QS(t[1]).offsetTop, y2 = parseInt(t[2]); console.log(y1, y2); document.documentElement.scrollTop = y1 + y2; } else if (t[0] == 'igot' || t[0] == 'ilost') { clmod(QS(t[1] + '>iframe'), 'focus', t[0] == 'igot'); } else if (t[0] == 'imshow') { thegrid.imshow(e.data.slice(7)); } else if (t[0] == 'goto') { var t = e.data.replace(/^[^ ]+ [^ ]+ /, '').split(/[?&]/)[0]; treectl.goto(t, !!t); } } catch (ex) { console.log('msg-err: ' + ex); } }, false); if (sb_lg && logues.length) { if (logues[1] === Ls.eng.f_empty) logues[1] = L.f_empty; sandbox(ebi('pro'), sb_lg, sba_lg, '', logues[0]); sandbox(ebi('epi'), sb_lg, sba_lg, '', logues[1]); } (function () { try { var tr = ebi('files').tBodies[0].rows; for (var a = 0; a < tr.length; a++) { var td = tr[a].cells[1], ao = td.firstChild, href = noq_href(ao), isdir = href.endsWith('/'), txt = ao.textContent; td.setAttribute('sortv', (isdir ? '\t' : '') + txt); } } catch (ex) { } })(); function ev_row_tgl(e) { ev(e); filecols.toggle(this.parentElement.parentElement.getElementsByTagName('span')[0].textContent); } var unpost = (function () { ebi('op_unpost').innerHTML = ( L.un_m1 + ' –
          ' + L.un_upd + '' + '

          ' + L.un_m4 + ' ' + L.un_ulist + ' / ' + L.un_ucopy + '' + '

          ' + L.un_flt + ' ' + L.un_fclr + '

          ' + '
          ' ); var r = {}, ct = ebi('unpost'), filt = ebi('unpost_filt'); r.files = []; r.me = null; r.load = function () { var me = Date.now(), html = []; function unpost_load_cb() { if (!xhrchk(this, L.fu_xe1, L.fu_xe2)) return ebi('op_unpost').innerHTML = L.fu_xe1; try { var ores = JSON.parse(this.responseText); } catch (ex) { return ebi('op_unpost').innerHTML = '

          ' + L.badreply + ':

          ' + unpre(this.responseText); } if (ores.nou) html.push('

          ' + L.un_nou + '

          '); if (ores.noc) html.push('

          ' + L.un_noc + '

          '); var res = ores.f; if (res.length) { if (ores.of) html.push("

          " + L.un_max); else html.push("

          " + L.un_avail.format(ores.nc, ores.nu)); html.push("
          " + L.un_m2 + "

          "); html.push(""); } else html.push('-- ' + (filt.value ? L.un_no2 : L.un_no1) + ''); var mods = [10, 100, 1000]; for (var a = 0; a < res.length; a++) { for (var b = 0; b < mods.length; b++) if (a % mods[b] == 0 && res.length > a + mods[b] / 10) html.push( ''); var done = res[a].pd === undefined; html.push( '' + '' + '' + (done ? '' : '') + ''); } html.push("
          timesizedonefile
          ' + '' + L.un_next.format(Math.min(mods[b], res.length - a)) + '
          ' + (done ? L.un_del : L.un_abrt) + '' + unix2ui(res[a].at) + '' + ('' + res[a].sz).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + '100%' + res[a].pd + '%' + linksplit(res[a].vp).join(' / ') + '
          "); ct.innerHTML = html.join('\n'); r.files = res; r.me = me; } var q = get_evpath() + '?ups'; if (filt.value) q += '&filter=' + uricom_enc(filt.value, true); var xhr = new XHR(); xhr.open('GET', q, true); xhr.onload = xhr.onerror = unpost_load_cb; xhr.send(); ct.innerHTML = "

          " + L.un_m3 + "

          "; }; function linklist() { var ret = [], base = location.origin.replace(/\/$/, ''); for (var a = 0; a < r.files.length; a++) ret.push(base + r.files[a].vp); return ret.join('\r\n'); } function unpost_delete_cb() { if (this.status !== 200) { var msg = unpre(this.responseText); toast.err(9, L.un_derr + msg); return; } for (var a = this.n; a < this.n2; a++) { var o = QSA('#op_unpost a.n' + a); for (var b = 0; b < o.length; b++) { var o2 = o[b].closest('tr'); o2.parentNode.removeChild(o2); } } toast.ok(5, this.responseText); if (!QS('#op_unpost a[me]')) goto_unpost(); var fi = window.up2k && up2k.st.files; if (fi && fi.length < 9) { for (var a = 0; a < fi.length; a++) { var f = fi[a]; if (!f.done && (f.rechecks || f.want_recheck) && !has(up2k.st.todo.handshake, f) && !has(up2k.st.busy.handshake, f) ) { up2k.st.todo.handshake.push(f); up2k.ui.seth(f.n, 2, L.u_hashdone); up2k.ui.seth(f.n, 1, '📦 wait'); up2k.ui.move(f.n, 'bz'); } } } } ct.onclick = function (e) { var tgt = e.target.closest('a[me]'); if (!tgt) return; if (!tgt.getAttribute('href')) return; ev(e); var ame = tgt.getAttribute('me'); if (ame != r.me) return toast.err(0, L.un_f5); var n = parseInt(tgt.className.slice(1)), n2 = parseInt(tgt.getAttribute('n2') || n + 1), req = []; for (var a = n; a < n2; a++) { var links = QSA('#op_unpost a.n' + a); if (!links.length) continue; var f = r.files[a]; if (f.k == 'u') { var vp = vsplit(f.vp.split('?')[0]), dfn = uricom_dec(vp[1]); for (var iu = 0; iu < up2k.st.files.length; iu++) { var uf = up2k.st.files[iu]; if (uf.name == dfn && uf.purl == vp[0]) return modal.alert(L.un_uf5); } } req.push(uricom_dec(f.vp.split('?')[0])); for (var b = 0; b < links.length; b++) { links[b].removeAttribute('href'); links[b].innerHTML = '[busy]'; } } toast.show('inf r', 0, L.un_busy.format(req.length)); var xhr = new XHR(); xhr.n = n; xhr.n2 = n2; xhr.open('POST', SR + '/?delete&unpost&lim=' + req.length, true); xhr.onload = xhr.onerror = unpost_delete_cb; xhr.send(JSON.stringify(req)); }; var tfilt = null; filt.oninput = function () { clearTimeout(tfilt); tfilt = setTimeout(r.load, 250); }; ebi('unpost_nofilt').onclick = function (e) { ev(e); filt.value = ''; r.load(); }; ebi('unpost_refresh').onclick = function (e) { ev(e); goto('unpost'); }; ebi('unpost_ulist').onclick = function (e) { ev(e); modal.alert(linklist()); }; ebi('unpost_ucopy').onclick = function (e) { ev(e); var txt = linklist(); cliptxt(txt + '\n', function () { toast.inf(5, L.un_clip.format(txt.split('\n').length)); }); }; return r; })(); function goto_unpost(e) { unpost.load(); } function wintitle(txt, noname) { if (txt === undefined) txt = ''; if (s_name && !noname) txt = s_name + ' ' + txt; txt += uricom_dec(get_evpath()).slice(1, -1).split('/').pop(); document.title = txt || "copyparty"; } ebi('path').onclick = function (e) { if (ctrl(e)) return true; var a = e.target.closest('a[href]'); if (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/')) return; thegrid.setvis(true); treectl.reqls(a, true); return ev(e); }; var scroll_y = -1; var scroll_vp = '\n'; var scroll_obj = null; function persist_scroll() { var obj = scroll_obj; if (!obj) { var o1 = document.getElementsByTagName('html')[0]; var o2 = document.body; obj = o1.scrollTop > o2.scrollTop ? o1 : o2; } var y = obj.scrollTop; if (y > 0) scroll_obj = obj; scroll_y = y; scroll_vp = get_evpath(); } function restore_scroll() { if (get_evpath() == scroll_vp && scroll_obj && scroll_obj.scrollTop < 1) scroll_obj.scrollTop = scroll_y; } ebi('files').onclick = ebi('docul').onclick = function (e) { if (!treectl.csel && e && (ctrl(e) || e.shiftKey)) return true; if (!showfile.active()) persist_scroll(); var tgt = e.target.closest('a[id]'); if (tgt && tgt.getAttribute('id').indexOf('f-') === 0 && tgt.textContent.endsWith('/')) { var el = treectl.find(tgt.textContent.slice(0, -1)); if (el) { el.click(); return ev(e); } treectl.reqls(tgt.getAttribute('href'), true); return ev(e); } if (tgt && /\.PARTIAL(\?|$)/.exec('' + tgt.getAttribute('href')) && !window.partdlok) { ev(e); modal.confirm(L.f_partial, function () { window.partdlok = 1; tgt.click(); }, null); } tgt = e.target.closest('a[hl]'); if (tgt) { var a = ebi(tgt.getAttribute('hl')), href = a.getAttribute('href'), fun = function () { showfile.show(href, tgt.getAttribute('lang')); }, tfun = function () { bcfg_set('taildoc', showfile.taildoc = true); fun(); }, szs = ft2dict(a.closest('tr'))[0].sz, sz = parseInt(szs.replace(/[, ]/g, '')); if (sz < 1024 * 1024 || showfile.taildoc) fun(); else modal.confirm(L.f_bigtxt.format(f2f(sz / 1024 / 1024, 1)), fun, function() { modal.confirm(L.f_bigtxt2, tfun, null)}); return ev(e); } tgt = e.target.closest('a'); if (tgt && tgt.closest('li.bn')) { thegrid.setvis(true); treectl.goto(tgt.getAttribute('href'), true); return ev(e); } }; var rcm = (function () { if (MOBILE) return {enabled: false} var r = {}; bcfg_bind(r, 'enabled', 'rcm_en', drcm.charAt(0)=='y'); bcfg_bind(r, 'double', 'rcm_db', drcm.charAt(1)=='y'); var menu = ebi('rcm'); var nsFile = { elem: null, type: null, path: null, dpath: null, url: null, id: null, name: null, no_dsel: false }; var selFile = jcp(nsFile); function mktemp(is_dir) { qsr('#rcm_tmp'); if (!thegrid.en) { var row = mknod('tr', 'rcm_tmp', '-new-'); QS("#files tbody").appendChild(row); } else { var row = mknod('a', 'rcm_tmp', ''); if (is_dir) row.className = 'dir'; row.style.display = 'flex'; QS("#ggrid").appendChild(row); } function sendit(name) { name = ('' + name).trim(); if (!name) return; var data = new FormData(); data.set("act", is_dir ? "mkdir" : "new_md"); data.set("name", name); var req = new XHR(); req.open("POST", get_evpath()); req.onload = req.onerror = function() { if (req.status == 405 || req.status == 500) return toast.err(3, "a " + (is_dir ? "folder" : "file") + " with that name already exists."); if (req.status < 200 || req.status > 399) return toast.err(3, "couldn't create " + (is_dir ? "folder" : "file") + ":
          " + esc(req.responseText) + ''); treectl.goto(); }; req.send(data); } var input = ebi("tempname"); input.onblur = function() { sendit(input.value); // Chrome blurs elements when calling remove for some reason input.onblur = null; row.remove(); }; input.onkeydown = function(e) { if (e.key == "Enter") sendit(input.value); if (e.key == "Enter" || e.key == "Escape") { input.onblur = null; row.remove(); ev(e); } }; input.focus(); } var opts = QSA('#rcm a'); for (var i = 0; i < opts.length; i++) { opts[i].onclick = function(e) { ev(e); switch(e.target.id.slice(1)) { case 'opn': var a = mknod('a'); a.href = selFile.url; a.target = selFile.type == "dir" ? '' : '_blank'; a.click(); break; case 'ply': selFile.type == 'gf' ? thegrid.imshow(selFile.name) : play('f-' + selFile.id); break; case 'pla': play('f-' + selFile.id); break; case 'txt': showfile.show(selFile.name); break; case 'md': location = selFile.path + (has(selFile.path, '?') ? '&v' : '?v'); break; case 'cpl': cliptxt(selFile.url, function() {toast.ok(2, L.clipped)}); break; case 'dl': ebi('seldl').click(); break; case 'zip': ebi('selzip').click(); break; case 'del': fileman.delete(); break; case 'cut': fileman.cut(); break; case 'cpy': fileman.cpy(); break; case 'pst': fileman.paste(); fileman.clip = []; break; case 'rnm': fileman.rename(); break; case 'nfo': mktemp(true); break; case 'nfi': mktemp(); break; case 'sal': msel.evsel(null, true); selFile.no_dsel = true; break; case 'sin': msel.evsel(null, 't'); break; case 'shr': fileman.share(); break; } r.hide(true); }; } function show(x, y, target, isGrid) { selFile = jcp(nsFile); if (target) { var file = target.closest("#files tbody tr"); if (isGrid && target.matches && target.matches('#ggrid > a')) { var ref = ebi(target.getAttribute('ref')); file = ref && ref.closest('#files tbody tr'); } var fa = file && file.children[1].querySelector('a[id]'); if (fa && fa.id != 'unsearch') { selFile.no_dsel = clgot(file, "sel"); clmod(file, "sel", true); selFile.elem = file; selFile.url = fa.href; selFile.path = basenames(selFile.url).replace(/(&|\?)v/, ''); var url = selFile.url.split("?")[0], vsp = vsplit(url); selFile.dpath = vsp[0]; selFile.name = vsp[1]; if (url.endsWith("/")) selFile.type = "dir"; else { var lead = file.firstChild.firstChild; if (lead.id === undefined) selFile.type = "tf"; else { selFile.id = lead.id.split('-')[1]; selFile.type = lead.innerHTML[0] == '(' ? 'gf' : lead.id.split('-')[0]; } } } } msel.selui(); var has_sel = msel.getsel().length; var has_clip = fileman.clip.length; clmod(ebi('ropn'), 'hide', !selFile.path); clmod(ebi('rply'), 'hide', selFile.type != 'gf' && selFile.type != 'af'); clmod(ebi('rpla'), 'hide', selFile.type != 'gf'); clmod(ebi('rtxt'), 'hide', !selFile.id); clmod(ebi('rs1'), 'hide', !selFile.path); clmod(ebi('rmd'), 'hide', !selFile.name || selFile.name.slice(-3) != ".md"); clmod(ebi('rcpl'), 'hide', !selFile.path); clmod(ebi('rdl'), 'hide', !has_sel); clmod(ebi('rzip'), 'hide', !has_sel); clmod(ebi('rs2'), 'hide', !has_sel); clmod(ebi('rcut'), 'hide', !has_sel); clmod(ebi('rdel'), 'hide', !has_sel); clmod(ebi('rcpy'), 'hide', !has_sel); clmod(ebi('rpst'), 'hide', !has_clip); clmod(ebi('rrnm'), 'hide', !has_sel); clmod(ebi('rs3'), 'hide', !has_sel); clmod(ebi('rs4'), 'hide', !has_sel && !has(perms, "write")); var shr = ebi('rshr'); clmod(shr, 'hide', !can_shr || !get_evpath().indexOf(have_shr)); shr.innerHTML = has_sel ? L.rc_shs : L.rc_shf; menu.style.left = x + 5 + 'px'; menu.style.top = y + 5 + 'px'; menu.style.display = 'block'; menu.focus(); } r.hide = function(force) { if (!menu.style.display || (!force && menu.contains(document.activeElement))) return; if (selFile.elem && !selFile.no_dsel) { clmod(selFile.elem, "sel", false); msel.selui(); } selFile = jcp(nsFile); menu.style.display = ''; } ebi('wrap').oncontextmenu = function(e) { if (!r.enabled || e.shiftKey || (r.double && menu.style.display) || /doc=/.exec(location.search)) { r.hide(true); return true; } r.hide(true); if (selFile.elem && !selFile.no_dsel) { clmod(selFile.elem, "sel", false); msel.selui(); } ev(e); var gfile = thegrid.en && e.target && e.target.closest('#ggrid > a'); show(xscroll() + e.clientX, yscroll() + e.clientY, gfile || e.target, gfile); return false; }; menu.onblur = function() {setTimeout(r.hide)}; return r; })(); function reload_mp() { if (mp && mp.au) { mpo.au = mp.au; mpo.au2 = mp.au2; mpo.acs = mp.acs; mpo.fau = mp.fau; mpl.unbuffer(); } var plays = QSA('tr>td:first-child>a.play'); for (var a = plays.length - 1; a >= 0; a--) plays[a].parentNode.innerHTML = '-'; mp = new MPlayer(); if (mp.au && mp.au.tid && mp.au.evp == get_evpath()) { var el = QS('a#a' + mp.au.tid); if (el) clmod(el, 'act', 1); el = el && el.closest('tr'); if (el) clmod(el, 'play', 1); } setTimeout(pbar.onresize, 1); } function reload_browser() { filecols.set_style(); var parts = get_evpath().split('/'), rm = ebi('entree'), ftab = ebi('files'), link = '', o; while (rm.nextSibling) rm.parentNode.removeChild(rm.nextSibling); for (var a = 0; a < parts.length - 1; a++) { link += parts[a] + '/'; var link2 = dks[link] ? addq(link, 'k=' + dks[link]) : link; o = mknod('a'); o.setAttribute('href', link2); o.textContent = uricom_dec(parts[a]) || '/'; ebi('path').appendChild(mknod('i')); ebi('path').appendChild(o); } reload_mp(); try { showsort(ftab); } catch (ex) { } makeSortable(ftab, function () { msel.origin_id(null); msel.load(true); thegrid.setdirty(); mp.read_order(); }); var ns = ['pro', 'epi', 'lazy'] for (var a = 0; a < ns.length; a++) clmod(ebi(ns[a]), 'hidden', ebi('unsearch')); if (up2k) up2k.set_fsearch(); thegrid.setdirty(); msel.render(); } (function() { var is_selma = false; var dragging = false; var startx, starty; var fwrap = null; var selbox = null; var ttimer = null; var lpdelay = 250; var mvthresh = 44; function unbox() { qsr('.selbox'); ebi('gfiles').style.removeProperty('pointer-events') ebi('wrap').style.removeProperty('user-select') if (selbox) { console.log(selbox) window.getSelection().removeAllRanges(); } is_selma = false; dragging = false; fwrap = null; selbox = null; ttimer = null; } function getpp(e) { var touch = (e.touches && e.touches[0]) || e; return { x: touch.clientX, y: touch.clientY }; } function sel_toggle(el, m) { clmod(el, 'sel', m); var eref = el.getAttribute('ref'); if (eref) { var ehidden = ebi(eref); if (ehidden) { var tr = ehidden.closest('tr'); if (tr) clmod(tr, 'sel', m); } } } function bob(b1, b2) { return !(b1.right < b2.left || b1.left > b2.right || b1.bottom < b2.top || b1.top > b2.bottom); } function sel_start(e) { if (e.button !== 0 && e.type !== 'touchstart') return; if (!thegrid.en || !treectl.dsel) return; if (e.target.closest('#widget,#ops,.opview,.doc')) return; if (e.target.closest('#gfiles')) ebi('gfiles').style.userSelect = "none" var pos = getpp(e); startx = pos.x; starty = pos.y; is_selma = true; ttimer = null; if (e.type === 'touchstart') { ttimer = setTimeout(function() { ttimer = null; start_drag(); }, lpdelay); } } function start_drag() { if (dragging) return; dragging = true; selbox = document.createElement('div'); selbox.className = 'selbox'; document.body.appendChild(selbox); ebi('gfiles').style.pointerEvents = 'none'; } function sel_move(e) { if (!is_selma) return; var pos = getpp(e); var dist = Math.sqrt(Math.pow(pos.x - startx, 2) + Math.pow(pos.y - starty, 2)); if (e.type === 'touchmove' && ttimer) { if (dist > mvthresh) { clearTimeout(ttimer); ttimer = null; is_selma = false; } return; } if (!dragging && dist > mvthresh && !window.getSelection().toString()) { if (fwrap = e.target.closest('#wrap')) fwrap.style.userSelect = 'none'; else return; start_drag(); } if (!dragging || !selbox) return; ev(e); selbox.style.width = Math.abs(pos.x - startx) + 'px'; selbox.style.height = Math.abs(pos.y - starty) + 'px'; selbox.style.left = Math.min(pos.x, startx) + 'px'; selbox.style.top = Math.min(pos.y, starty) + 'px'; if (IE && window.getSelection) window.getSelection().removeAllRanges(); } function sel_end(e) { clearTimeout(ttimer); if (dragging && selbox) { var sbrect = selbox.getBoundingClientRect(); var faf = QSA('#ggrid a'); var sadmode = e.shiftKey ? true : e.altKey ? false : "t"; for (var a = 0, aa = faf.length; a < aa; a++) if (bob(sbrect, faf[a].getBoundingClientRect())) sel_toggle(faf[a], sadmode); msel.selui(); ev(e); } unbox(); } function dsel_init() { window.addEventListener('mousedown', sel_start); window.addEventListener('mousemove', sel_move); window.addEventListener('mouseup', sel_end); window.addEventListener('touchstart', sel_start, { passive: true }); window.addEventListener('touchmove', sel_move, { passive: false }); window.addEventListener('touchend', sel_end, { passive: true }); window.addEventListener('dragstart', function(e) { if (treectl.dsel && (is_selma || dragging)) { e.preventDefault(); } }); } dsel_init(); })(); var mpss = (function() { var r = {}, config, ssint, npaint = 0; r.load = function () { if (!afilt.ssg) return false; config = { vthresh: afilt.sscv[0], sthresh: afilt.sscv[1], etresh: afilt.sscv[2], sspeed: clamp(afilt.sscv[3], 0.15, 8.0), rspeed: 0.2, loopInterval: 25, }; return true; }; r.go = function () { if (!ssint && afilt.ssen && r.load()) ssint = setInterval(detectSilence, config.loopInterval); }; r.stop = function () { clearInterval(ssint); ssint = null; if (!mp) return; if (afilt.ssg) afilt.ssg.gain.value = 1.0; if (mp.au && mp.au._ss) mp.au.playbackRate = 1.0; if (mp.au2 && mp.au2._ss) mp.au2.playbackRate = 1.0; }; function detectSilence() { var ae = mp.au; ae._ss = true; var gain = afilt.ssg.gain; var duration = ae.duration || 0; var slimit = duration * (config.sthresh / 100); var elimit = duration * (1 - (config.etresh / 100)); var in_limits = ae.currentTime < slimit || ae.currentTime > elimit; var tspeed = 1.0; var tvol = 1.0; var is_silent = false; if (in_limits) { var analyser = afilt.ssa; var da = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(da); var maxvol = 0; for (var i = 0; i < da.length; i++) { if (da[i] > maxvol) maxvol = da[i]; } if (++npaint > 4) { npaint = 0; ebi('au_ss').innerHTML = maxvol; } if (maxvol < config.vthresh) { tspeed = config.sspeed; tvol = 0.0; is_silent = true; } } if (is_silent) { if (Math.abs(ae.playbackRate - tspeed) > 0.01) { ae.playbackRate += (tspeed - ae.playbackRate) * config.rspeed; } if (Math.abs(gain.value - tvol) > 0.01) { gain.value += (tvol - gain.value) * config.rspeed; } } else { ae.playbackRate = 1.0; gain.value = 1.0; } } return r; })(); treectl.hydrate(); J_BRW = 2; ================================================ FILE: copyparty/web/browser2.html ================================================ {{ title }} {%- if srv_info %}

          {{ srv_info }}

          {%- endif %} {%- if have_b_u %}


          {%- endif %} {%- if logues[0] %} {%- if sb_lg %}
          {{ logues[0][:2000]|e }}

          {%- else %}
          {{ logues[0][:2000] }}

          {%- endif %} {%- endif %} {%- for f in files %} {%- endfor %}
          c File Name Size Date
          parent folder--
          {{ f.lead }}{{ f.name|e }}{{ f.sz }}{{ f.dt }}
          {%- if logues[1] %} {%- if sb_lg %}
          {{ logues[1][:2000]|e }}

          {%- else %}
          {{ logues[1][:2000] }}

          {%- endif %} {%- endif %}

          control-panel

          ================================================ FILE: copyparty/web/cf.html ================================================ {{ s_doctitle }}

          please press F5 to reload the page

          sorry for the inconvenience

          ================================================ FILE: copyparty/web/dbg-audio.js ================================================ var ofun = audio_eq.apply.bind(audio_eq); audio_eq.apply = function () { var ac1 = mp.ac; ofun(); var ac = mp.ac, w = 2048, h = 256; if (!audio_eq.filters.length) { audio_eq.ana = null; return; } var can = ebi('fft_can'); if (!can) { can = mknod('canvas', 'fft_can'); can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001'; document.body.appendChild(can); can.width = w; can.height = h; } var cc = can.getContext('2d'); if (!ac) return; var ana = ac.createAnalyser(); ana.smoothingTimeConstant = 0; ana.fftSize = 8192; audio_eq.filters[0].connect(ana); audio_eq.ana = ana; var buf = new Uint8Array(ana.frequencyBinCount), colw = can.width / buf.length; cc.fillStyle = '#fc0'; function draw() { if (ana == audio_eq.ana) requestAnimationFrame(draw); ana.getByteFrequencyData(buf); cc.clearRect(0, 0, can.width, can.height); /*var x = 0, w = 1; for (var a = 0; a < buf.length; a++) { cc.fillRect(x, h - buf[a], w, h); x += w; }*/ var mul = Math.pow(w, 4) / buf.length; for (var x = 0; x < w; x++) { var a = Math.floor(Math.pow(x, 4) / mul), v = buf[a]; cc.fillRect(x, h - v, 1, v); } } draw(); }; audio_eq.apply(); ================================================ FILE: copyparty/web/idp.html ================================================ {{ s_doctitle }} {{ html_head }}
          refresh control-panel {%- for un, gn in rows %} {%- endfor %}
          forget user groups
          forget {{ un|e }} {{ gn|e }}
          {%- if not rows %} (there are no IdP users in the cache) {%- endif %}
          π {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/md.css ================================================ html, body { color: #333; background: #eee; font-family: sans-serif; font-family: var(--font-main), sans-serif; line-height: 1.5em; } html.y #helpbox a { color: #079; } html.z #helpbox a { color: #fc5; } #repl { position: absolute; top: 0; right: .5em; border: none; color: inherit; background: none; } #mtw { display: none; } #mw { margin: 0 auto; padding: 0 1.5em; } #toast { bottom: auto; top: 1.4em; } a { text-decoration: none; } #toc { margin: 0 1em; -ms-scroll-chaining: none; overscroll-behavior-y: none; } #toc ul { padding-left: 1em; } #toc>ul { text-align: left; padding-left: .5em; } #toc li { list-style-type: none; line-height: 1.2em; margin: .5em 0; } #toc a { color: #057; border: none; background: none; display: block; margin-left: -.3em; padding: .2em .3em; } #toc a.act { color: #fff; background: #07a; } .todo_pend, .todo_done { z-index: 99; position: relative; display: inline-block; font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; font-weight: bold; font-size: 1.3em; line-height: .1em; margin: -.5em 0 -.5em -.85em; top: .1em; color: #b29; } .todo_done { color: #6b3; text-shadow: .02em 0 0 #6b3; } blink { animation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite; } @keyframes blinker { 10% { opacity: 0; } 60% { opacity: 1; } } .mdo pre { counter-reset: precode; } .mdo pre code { counter-increment: precode; display: inline-block; border: none; border-bottom: 1px solid #cdc; min-width: calc(100% - .6em); } .mdo pre code:last-child { border-bottom: none; } .mdo pre code::before { content: counter(precode); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; display: inline-block; text-align: right; font-size: .75em; color: #48a; width: 4em; padding-right: 1.5em; margin-left: -5.5em; } @media screen { html, body { margin: 0; padding: 0; outline: 0; border: none; width: 100%; height: 100%; } #mw { margin: 0 auto; right: 0; } #mp { max-width: 52em; margin-bottom: 6em; } #mn { padding: 1.3em 0 .7em 1em; border-bottom: 1px solid #ccc; background: #eee; z-index: 10; width: calc(100% - 1em); } #mn a { color: #444; background: none; margin: 0 0 0 -.2em; padding: .3em 0 .3em .4em; text-decoration: none; border: none; /* ie: */ border-bottom: .1em solid #777\9; margin-right: 1em\9; } #mn a:first-child { padding-left: .5em; } #mn a:last-child { padding-right: .5em; } #mn a:not(:last-child)::after { content: ''; width: 1.05em; height: 1.05em; margin: -.2em .3em -.2em -.4em; display: inline-block; border: 1px solid rgba(154,154,154,0.6); border-width: .2em .2em 0 0; transform: rotate(45deg); } #mn a:hover { color: #000; text-decoration: underline; } #mh { padding: .4em 1em; position: relative; width: 100%; width: calc(100% - 3em); background: #eee; z-index: 9; top: 0; } #mh a { color: #444; background: none; text-decoration: underline; margin: 0 .1em; padding: 0 .3em; border: none; } #mh a:hover { color: #000; background: #ddd; } #toolsbox { overflow: hidden; display: inline-block; background: #eee; height: 1.5em; padding: 0 .2em; margin: 0 .2em; position: absolute; } #toolsbox.open { height: auto; overflow: visible; background: #eee; box-shadow: 0 .2em .2em #ccc; padding-bottom: .2em; } #toolsbox a { display: block; } #toolsbox a+a { text-decoration: none; } #lno { position: absolute; right: 0; } html.z, html.z body { background: #222; color: #ccc; } html.z #toc a { color: #ccc; border-left: .4em solid #444; border-bottom: .1em solid #333; } html.z #toc a.act { color: #fff; border-left: .4em solid #3ad; } html.z #toc li { border-width: 0; } html.z #mn a { color: #ccc; } html.z #mn { border-bottom: 1px solid #333; } html.z #mn, html.z #mh { background: #222; } html.z #mh a { color: #ccc; background: none; } html.z #mh a:hover { background: #333; color: #fff; } html.z #toolsbox { background: #222; } html.z #toolsbox.open { box-shadow: 0 .2em .2em #069; border-radius: 0 0 .4em .4em; } } @media screen and (min-width: 66em) { #mw { position: fixed; overflow-y: auto; left: 14em; left: calc(100% - 55em); max-width: none; bottom: 0; scrollbar-color: #eb0 #f7f7f7; } #toc { width: 13em; width: calc(100% - 55.3em); max-width: 30em; background: #eee; position: fixed; overflow-y: auto; top: 0; left: 0; bottom: 0; padding: 0; margin: 0; scrollbar-color: #eb0 #f7f7f7; box-shadow: 0 0 1em rgba(0,0,0,0.1); border-top: 1px solid #d7d7d7; } #toc li { border-left: .3em solid #ccc; } #toc::-webkit-scrollbar-track { background: #f7f7f7; } #toc::-webkit-scrollbar { background: #f7f7f7; width: .8em; } #toc::-webkit-scrollbar-thumb { background: #eb0; } html.z #toc { background: #282828; border-top: 1px solid #2c2c2c; box-shadow: 0 0 1em #181818; } html.z #toc, html.z #mw { scrollbar-color: #b80 #282828; } html.z #toc::-webkit-scrollbar-track { background: #282828; } html.z #toc::-webkit-scrollbar { background: #282828; width: .8em; } html.z #toc::-webkit-scrollbar-thumb { background: #b80; } } @media screen and (min-width: 85.5em) { #toc { width: 30em } #mw { left: 30.5em } } @media print { @page { size: A4; padding: 0; margin: .5in .6in; mso-header-margin: .6in; mso-footer-margin: .6in; mso-paper-source: 0; } .mdo a { color: #079; text-decoration: none; border-bottom: .07em solid #4ac; padding: 0 .3em; } #repl { display: none; } #toc>ul { border-left: .1em solid #84c4dd; } #mn, #mh { display: none; } html, body, #toc, #mw { margin: 0 !important; word-break: break-word; width: 52em; } #toc { margin-left: 1em !important; } #toc a { color: #000 !important; } #toc a::after { /* hopefully supported by browsers eventually */ content: leader('.') target-counter(attr(href), page); } a[ctr]::before { content: attr(ctr) '. '; } .mdo h1 { margin: 2em 0; } .mdo h2 { margin: 2em 0 0 0; } .mdo h1, .mdo h2, .mdo h3 { page-break-inside: avoid; } .mdo h1::after, .mdo h2::after, .mdo h3::after { content: 'orz'; color: transparent; display: block; line-height: 1em; padding: 4em 0 0 0; margin: 0 0 -5em 0; } .mdo p { page-break-inside: avoid; } .mdo table { page-break-inside: auto; } .mdo tr { page-break-inside: avoid; page-break-after: auto; } .mdo thead { display: table-header-group; } .mdo tfoot { display: table-footer-group; } #mp a.vis::after { content: ' (' attr(href) ')'; border-bottom: 1px solid #bbb; color: #444; } .mdo blockquote { border-color: #555; } .mdo code { border-color: #bbb; } .mdo pre, .mdo pre code { border-color: #999; } .mdo pre code::before { color: #058; } html.z .mdo a { color: #000; } html.z .mdo pre, html.z .mdo code { color: #240; } html.z .mdo p>em, html.z .mdo li>em, html.z .mdo td>em { color: #940; } } ================================================ FILE: copyparty/web/md.html ================================================ 📝 {{ title }} {%- if edit %} {%- endif %} {{ html_head }}
          Loading
          if you're still reading this, check that javascript is allowed
          π {%- if edit %}
          {%- endif %} {%- if edit %} {%- endif %} {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/md.js ================================================ "use strict"; var J_MD = 1; var dom_toc = ebi('toc'), dom_wrap = ebi('mw'), dom_hbar = ebi('mh'), dom_nav = ebi('mn'), dom_pre = ebi('mp'), dom_src = ebi('mt'), dom_navtgl = ebi('navtoggle'), hash0 = location.hash; // chrome 49 needs this var chromedbg = function () { console.log(arguments); } // null-logger var dbg = function () { }; // replace dbg with the real deal here or in the console: // dbg = chromedbg; // dbg = console.log; // dodge browser issues (function () { if (UA.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(UA)) { // necessary on ff-68.7 at least var s = mknod('style'); s.innerHTML = '@page { margin: .5in .6in .8in .6in; }'; console.log(s.innerHTML); document.head.appendChild(s); } })(); // add navbar (function () { var parts = (get_evpath().slice(0, -1).split('?')[0] + '?v').split('/'), link = '', o; for (var a = 0, aa = parts.length - 1; a <= aa; a++) { link += parts[a] + (a < aa ? '/' : ''); o = mknod('a'); o.setAttribute('href', link); o.textContent = uricom_dec(parts[a].split('?')[0]) || 'top'; dom_nav.appendChild(o); } })(); // image load handler var img_load = (function () { var r = {}; r.callbacks = []; var fire = function () { for (var a = 0; a < r.callbacks.length; a++) r.callbacks[a](); } var timeout = null; r.done = function () { clearTimeout(timeout); timeout = setTimeout(fire, 500); }; return r; })(); // faster than replacing the entire html (chrome 1.8x, firefox 1.6x) function copydom(src, dst, lv) { var sc = src.childNodes, dc = dst.childNodes; if (sc.length !== dc.length) { dbg("replace L%d (%d/%d) |%d|", lv, sc.length, dc.length, src.innerHTML.length); dst.innerHTML = src.innerHTML; return; } var rpl = []; for (var a = sc.length - 1; a >= 0; a--) { var st = sc[a].tagName || sc[a].nodeType, dt = dc[a].tagName || dc[a].nodeType; if (st !== dt) { dbg("replace L%d (%d/%d) type %s/%s", lv, a, sc.length, st, dt); dst.innerHTML = src.innerHTML; return; } var sa = sc[a].attributes || [], da = dc[a].attributes || []; if (sa.length !== da.length) { dbg("replace L%d (%d/%d) attr# %d/%d", lv, a, sc.length, sa.length, da.length); rpl.push(a); continue; } var dirty = false; for (var b = sa.length - 1; b >= 0; b--) { var name = sa[b].name, sv = sa[b].value, dv = dc[a].getAttribute(name); if (name == "data-ln" && sv !== dv) { dc[a].setAttribute(name, sv); continue; } if (sv !== dv) { dbg("replace L%d (%d/%d) attr %s [%s] [%s]", lv, a, sc.length, name, sv, dv); dirty = true; break; } } if (dirty) rpl.push(a); } // TODO pure guessing if (rpl.length > sc.length / 3) { dbg("replace L%d fully, %s (%d/%d) |%d|", lv, rpl.length, sc.length, src.innerHTML.length); dst.innerHTML = src.innerHTML; return; } // repl is reversed; build top-down var nbytes = 0; for (var a = rpl.length - 1; a >= 0; a--) { var i = rpl[a], prop = sc[i].nodeType == 1 ? 'outerHTML' : 'nodeValue'; var html = sc[i][prop]; dc[i][prop] = html; nbytes += html.length; } if (nbytes > 0) dbg("replaced %d bytes L%d", nbytes, lv); for (var a = 0; a < sc.length; a++) copydom(sc[a], dc[a], lv + 1); if (src.innerHTML !== dst.innerHTML) { dbg("setting %d bytes L%d", src.innerHTML.length, lv); dst.innerHTML = src.innerHTML; } } md_plug_err = function (ex, js) { qsr('#md_errbox'); if (!ex) return; var msg = (ex + '').split('\n')[0]; var ln = ex.lineNumber; var o = null; if (ln) { msg = "Line " + ln + ", " + msg; var lns = js.split('\n'); if (ln < lns.length) { o = mknod('span'); o.style.cssText = "color:#ac2;font-size:.9em;font-family:'scp',monospace,monospace;display:block"; o.textContent = lns[ln - 1]; } } var errbox = mknod('div', 'md_errbox'); errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5' errbox.textContent = msg; errbox.onclick = function () { modal.alert('
          ' + esc(ex.stack) + '
          '); }; if (o) { errbox.appendChild(o); errbox.style.padding = '.25em .5em'; } dom_nav.appendChild(errbox); try { console.trace(); } catch (ex2) { } } function convert_markdown(md_text, dest_dom) { md_text = md_text.replace(/\r/g, ''); md_plug_err(null); md_text = load_md_plug(md_text, 'pre'); md_text = load_md_plug(md_text, 'post'); var marked_opts = { //headerPrefix: 'h-', breaks: !md_no_br, gfm: true }; var ext = md_plug.pre; if (ext) Object.assign(marked_opts, ext[0]); try { var md_html = marked.parse(md_text, marked_opts); if (!have_emp) md_html = DOMPurify.sanitize(md_html); } catch (ex) { if (IE) { dest_dom.innerHTML = 'IE cannot into markdown ;_;'; return false; } if (ext) md_plug_err(ex, ext[1]); throw ex; } var md_dom = dest_dom; try { md_dom = new DOMParser().parseFromString(md_html, "text/html").body; } catch (ex) { md_dom.innerHTML = md_html; window.copydom = noop; } var nodes = md_dom.getElementsByTagName('a'); for (var a = nodes.length - 1; a >= 0; a--) { var href = nodes[a].getAttribute('href'); var txt = nodes[a].innerHTML; if (/\.[Mm][Dd]$/.test(href)) { var o = new URL(href, location.href).origin; if (!o || o == location.origin) nodes[a].href = href + '?v'; } if (!txt) nodes[a].textContent = href; else if (href !== txt && !nodes[a].className) nodes[a].className = 'vis'; } // todo-lists (should probably be a marked extension) nodes = md_dom.getElementsByTagName('input'); for (var a = nodes.length - 1; a >= 0; a--) { var dom_box = nodes[a]; if (dom_box.getAttribute('type') !== 'checkbox') continue; var dom_li = dom_box.parentNode; var done = dom_box.getAttribute('checked'); done = done !== null; var clas = done ? 'done' : 'pend'; var char = done ? 'Y' : 'N'; dom_li.className = 'task-list-item'; dom_li.style.listStyleType = 'none'; var html = dom_li.innerHTML; dom_li.innerHTML = '' + char + '' + html.slice(html.indexOf('>') + 1); } // separate for each line in
              nodes = md_dom.getElementsByTagName('pre');
              for (var a = nodes.length - 1; a >= 0; a--) {
                  var el = nodes[a];
          
                  var is_precode =
                      el.tagName == 'PRE' &&
                      el.childNodes.length === 1 &&
                      el.childNodes[0].tagName == 'CODE';
          
                  if (!is_precode)
                      continue;
          
                  var nline = parseInt(el.getAttribute('data-ln')) + 1;
                  var lines = el.innerHTML.replace(/\n<\/code>$/i, '').split(/\n/g);
                  for (var b = 0; b < lines.length - 1; b++)
                      lines[b] += '\n';
          
                  el.innerHTML = lines.join('');
              }
          
              // self-link headers
              var id_seen = {},
                  dyn = md_dom.getElementsByTagName('*');
          
              nodes = [];
              for (var a = 0, aa = dyn.length; a < aa; a++)
                  if (/^[Hh]([1-6])/.exec(dyn[a].tagName) !== null)
                      nodes.push(dyn[a]);
          
              for (var a = 0; a < nodes.length; a++) {
                  el = nodes[a];
                  var id = el.getAttribute('id'),
                      orig_id = id;
          
                  if (id_seen[id]) {
                      for (var n = 1; n < 4096; n++) {
                          id = orig_id + '-' + n;
                          if (!id_seen[id])
                              break;
                      }
                      el.setAttribute('id', id);
                  }
                  id_seen[id] = 1;
                  el.innerHTML = '' + el.innerHTML + '';
              }
          
              ext = md_plug.post;
              if (ext && ext[0].render)
                  try {
                      ext[0].render(md_dom);
                  }
                  catch (ex) {
                      md_plug_err(ex, ext[1]);
                  }
          
              copydom(md_dom, dest_dom, 0);
          
              var imgs = dest_dom.getElementsByTagName('img');
              for (var a = 0, aa = imgs.length; a < aa; a++)
                  imgs[a].onload = img_load.done;
          
              if (ext && ext[0].render2)
                  try {
                      ext[0].render2(dest_dom);
                  }
                  catch (ex) {
                      md_plug_err(ex, ext[1]);
                  }
          
              if (hash0)
                  setTimeout(function () {
                      try {
                          QS(hash0).scrollIntoView();
                          hash0 = '';
                      }
                      catch (ex) { }
                  }, 1);
              
              return true;
          }
          
          
          function init_toc() {
              qsr('#ml');
          
              var anchors = [];  // list of toc entries, complex objects
              var anchor = null; // current toc node
              var html = [];     // generated toc html
              var lv = 0;        // current indentation level in the toc html
              var ctr = [0, 0, 0, 0, 0, 0];
          
              var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
              var manip_nodes = [];
              for (var a = 0, aa = manip_nodes_dyn.length; a < aa; a++)
                  manip_nodes.push(manip_nodes_dyn[a]);
          
              for (var a = 0, aa = manip_nodes.length; a < aa; a++) {
                  var elm = manip_nodes[a];
                  var m = /^[Hh]([1-6])/.exec(elm.tagName);
                  var is_header = m !== null;
                  if (is_header) {
                      var nlv = m[1];
                      while (lv < nlv) {
                          html.push('
            '); lv++; } while (lv > nlv) { html.push('
          '); lv--; } ctr[lv - 1]++; for (var b = lv; b < 6; b++) ctr[b] = 0; elm.childNodes[0].setAttribute('ctr', ctr.slice(0, lv).join('.')); var elm2 = elm.cloneNode(true); elm2.childNodes[0].textContent = elm.textContent; while (elm2.childNodes.length > 1) elm2.removeChild(elm2.childNodes[1]); html.push('
        • ' + elm2.innerHTML + '
        • '); if (anchor != null) anchors.push(anchor); anchor = { elm: elm, kids: [], y: null }; } if (!is_header && anchor) anchor.kids.push(elm); } dom_toc.innerHTML = html.join('\n'); if (anchor != null) anchors.push(anchor); // copy toc links into the toc list var atoc = dom_toc.getElementsByTagName('a'); for (var a = 0, aa = anchors.length; a < aa; a++) anchors[a].lnk = atoc[a]; // collect vertical position of all toc items (headers in document) function freshen_offsets() { var top = yscroll(); for (var a = anchors.length - 1; a >= 0; a--) { var y = top + anchors[a].elm.getBoundingClientRect().top; y = Math.round(y * 10.0) / 10; if (anchors[a].y === y) break; anchors[a].y = y; } } // highlight the correct toc items + scroll into view function freshen_toclist() { if (anchors.length == 0) return; var ptop = yscroll(); var hit = anchors.length - 1; for (var a = 0; a < anchors.length; a++) { if (anchors[a].y >= ptop - 8) { //??? hit = a; break; } } var links = dom_toc.getElementsByTagName('a'); if (!anchors[hit].active) { for (var a = 0; a < anchors.length; a++) { if (anchors[a].active) { anchors[a].active = false; links[a].className = ''; } } anchors[hit].active = true; links[hit].className = 'act'; } var pane_height = parseInt(getComputedStyle(dom_toc).height); var link_bounds = links[hit].getBoundingClientRect(); var top = link_bounds.top - (pane_height / 6); var btm = link_bounds.bottom + (pane_height / 6); if (top < 0) dom_toc.scrollTop -= -top; else if (btm > pane_height) dom_toc.scrollTop += btm - pane_height; } function refresh() { freshen_offsets(); freshen_toclist(); } return { "refresh": refresh } } // "main" :p convert_markdown(dom_src.value, dom_pre); var toc = init_toc(); img_load.callbacks = [toc.refresh]; // scroll handler var redraw = (function () { var sbs = true; var onresize = function () { if (window.matchMedia) sbs = window.matchMedia('(min-width: 64em)').matches; var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px'; if (sbs) { dom_toc.style.top = y; dom_wrap.style.top = y; dom_toc.style.marginTop = '0'; } onscroll(); } var onscroll = function () { toc.refresh(); } window.onresize = onresize; window.onscroll = onscroll; dom_wrap.onscroll = onscroll; onresize(); return onresize; })(); dom_navtgl.onclick = function () { var hidden = dom_navtgl.innerHTML == 'hide nav'; dom_navtgl.innerHTML = hidden ? 'show nav' : 'hide nav'; dom_nav.style.display = hidden ? 'none' : 'block'; swrite('hidenav', hidden ? 1 : 0); redraw(); }; if (sread('hidenav') == 1) dom_navtgl.onclick(); if (window.tt && tt.init) tt.init(); J_MD = 2; ================================================ FILE: copyparty/web/md2.css ================================================ #toc { display: none; } #mtw { display: block; position: fixed; left: .5em; bottom: 0; width: calc(100% - 56em); } #mw { overflow-y: auto; position: fixed; bottom: 0; left: 0; } @media (min-width: 55em) { #mw {left:calc(100% - 55em)} } /* single-screen */ #mtw.preview, #mw.editor { opacity: 0; z-index: 1; } #mw.preview, #mtw.editor { z-index: 5; } #mtw.single, #mw.single { margin: 0; left: 1em; left: max(1em, calc((100% - 56em) / 2)); } #mtw.single { width: 55em; width: min(55em, calc(100% - 2em)); } #mtw.single.editor, #mw.single.editor { width: calc(100% - 1em); left: .5em; } #mp { position: relative; } #mt, #mtr { width: 100%; height: calc(100% - 1px); color: #444; background: #f7f7f7; border: 1px solid #999; outline: none; padding: 0; margin: 0; font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; word-wrap: break-word; /*ie*/ overflow-y: scroll; line-height: 1.3em; font-size: .9em; position: relative; scrollbar-color: #eb0 #f7f7f7; } html.z #mt { color: #eee; background: #222; border: 1px solid #777; scrollbar-color: #b80 #282828; } #mtr { position: absolute; top: 0; left: 0; } #save.force-save { color: #400; background: #f97; border-radius: .15em; } html.z #save.force-save { color: #fca; background: #720; } #save.disabled { opacity: .4; } #helpbox { background: #f7f7f7; border-radius: .4em; z-index: 9001; display: none; position: fixed; padding: 2em; top: 4em; overflow-y: auto; box-shadow: 0 .5em 2em #777; height: calc(100% - 12em); left: calc(50% - 15em); right: 0; width: 30em; } #helpclose { display: block; } html.z #helpbox { box-shadow: 0 .5em 2em #444; background: #222; border: 1px solid #079; border-width: 1px 0; } ================================================ FILE: copyparty/web/md2.js ================================================ "use strict"; var J_MD2 = 1; var sloc0 = '' + location, dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0); // server state var server_md = dom_src.value; // the non-ascii whitelist var esc_uni_whitelist = '\\n\\t\\x20-\\x7eÆØÅæøå'; var js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\''); // dom nodes var dom_swrap = ebi('mtw'); var dom_sbs = ebi('sbs'); var dom_nsbs = ebi('nsbs'); var dom_tbox = ebi('toolsbox'); var dom_ref = (function () { var d = mknod('div', 'mtr'); dom_swrap.appendChild(d); d = ebi('mtr'); // hide behind the textarea (offsetTop is not computed if display:none) dom_src.style.zIndex = '4'; d.style.zIndex = '3'; return d; })(); // line->scrollpos maps function genmapq(dom, query) { var ret = []; var last_y = -1; var parent_y = 0; var parent_n = null; var nodes = dom.querySelectorAll(query); for (var a = 0; a < nodes.length; a++) { var n = nodes[a]; var ln = parseInt(n.getAttribute('data-ln')); if (ln in ret) continue; var y = 0; var par = n.offsetParent; if (par && par != parent_n) { while (par && par != dom) { y += par.offsetTop; par = par.offsetParent; } if (par != dom) continue; parent_y = y; parent_n = n.offsetParent; } while (ln > ret.length) ret.push(null); y = parent_y + n.offsetTop; if (y <= last_y) //console.log('awawa'); continue; //console.log('%d %d (%d+%d)', a, y, parent_y, n.offsetTop); ret.push(y); last_y = y; } return ret; } var map_src = []; var map_pre = []; function genmap(dom, oldmap) { var find = nlines; while (oldmap && find-- > 0) { var tmap = genmapq(dom, '*[data-ln="' + find + '"]'); if (!tmap || !tmap.length) continue; var cy = tmap[find]; var oy = parseInt(oldmap[find]); if (cy + 24 > oy && cy - 24 < oy) return oldmap; console.log('map regen', dom.getAttribute('id'), find, oy, cy, oy - cy); break; } return genmapq(dom, '*[data-ln]'); } // input handler var action_stack = null; var nlines = 0; var draw_md = (function () { var delay = 1; var draw_md = function () { var t0 = Date.now(); var src = dom_src.value; convert_markdown(src, dom_pre); var lines = esc(src).replace(/\r/g, "").split('\n'); nlines = lines.length; var html = []; for (var a = 0; a < lines.length; a++) html.push('' + lines[a] + ""); dom_ref.innerHTML = html.join('\n'); map_src = genmap(dom_ref, map_src); map_pre = genmap(dom_pre, map_pre); clmod(ebi('save'), 'disabled', src.replace(/\r/g, "") == server_md.replace(/\r/g, "")); var t1 = Date.now(); delay = t1 - t0 > 100 ? 25 : 1; } var timeout = null; dom_src.oninput = function (e) { clearTimeout(timeout); timeout = setTimeout(draw_md, delay); if (action_stack) action_stack.push(); }; draw_md(); return draw_md; })(); // discard TOC callback, just regen editor scroll map img_load.callbacks = [function () { map_pre = genmap(dom_pre, map_pre); }]; // resize handler redraw = (function () { var onresize = function () { var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px'; dom_wrap.style.top = y; dom_swrap.style.top = y; dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px'; map_src = genmap(dom_ref, map_src); map_pre = genmap(dom_pre, map_pre); } var setsbs = function () { dom_wrap.className = ''; dom_swrap.className = ''; onresize(); } var modetoggle = function () { var mode = dom_nsbs.innerHTML; dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor'; mode += ' single'; dom_wrap.className = mode; dom_swrap.className = mode; onresize(); } window.onresize = onresize; window.onscroll = null; dom_wrap.onscroll = null; dom_sbs.onclick = setsbs; dom_nsbs.onclick = modetoggle; (IE ? modetoggle : onresize)(); return onresize; })(); // scroll handlers (function () { var skip_src = false, skip_pre = false; var scroll = function (src, srcmap, dst, dstmap) { var y = src.scrollTop; if (y < 8) { dst.scrollTop = 0; return; } if (y + 48 + src.clientHeight > src.scrollHeight) { dst.scrollTop = dst.scrollHeight - dst.clientHeight; return; } y += src.clientHeight / 2; var sy1 = -1, sy2 = -1, dy1 = -1, dy2 = -1; for (var a = 1; a < nlines + 1; a++) { if (srcmap[a] == null || dstmap[a] == null) continue; if (srcmap[a] > y) { sy2 = srcmap[a]; dy2 = dstmap[a]; break; } sy1 = srcmap[a]; dy1 = dstmap[a]; } if (sy1 == -1) return; var dy = dy1; if (sy2 != -1 && dy2 != -1) { var mul = (y - sy1) / (sy2 - sy1); dy = dy1 + (dy2 - dy1) * mul; } dst.scrollTop = dy - dst.clientHeight / 2; } dom_src.onscroll = function () { //dbg: dom_ref.scrollTop = dom_src.scrollTop; if (skip_src) { skip_src = false; return; } skip_pre = true; scroll(dom_src, map_src, dom_wrap, map_pre); }; dom_wrap.onscroll = function () { if (skip_pre) { skip_pre = false; return; } skip_src = true; scroll(dom_wrap, map_pre, dom_src, map_src); }; })(); // modification checker function Modpoll() { var r = { initial: true, skip_one: false, disabled: false }; r.periodic = function () { var skip = null; if (toast.visible) skip = 'toast'; else if (r.skip_one) skip = 'saved'; else if (r.disabled) skip = 'disabled'; if (skip) { console.log('modpoll skip, ' + skip); r.skip_one = false; return; } console.log('modpoll...'); var url = (location + '').split('?')[0] + '?_=' + Date.now(); var xhr = new XHR(); xhr.open('GET', url, true); xhr.responseType = 'text'; xhr.onload = xhr.onerror = r.cb; xhr.send(); }; r.cb = function () { if (r.disabled || r.skip_one) { console.log('modpoll abort'); return; } if (this.status !== 200) { console.log('modpoll err ' + this.status + ": " + this.responseText); return; } if (!this.responseText) return; var new_md = this.responseText, new_mt = this.getResponseHeader('X-Lastmod3') || r.lastmod, server_ref = server_md.replace(/\r/g, ''), server_now = new_md.replace(/\r/g, ''); // firefox bug: sometimes get stale text even if copyparty sent a 200 if (r.initial && server_ref != server_now) return modal.confirm('Your browser decided to show an outdated copy of the document!\n\nDo you want to load the latest version from the server instead?', function () { dom_src.value = server_md = new_md; last_modified = new_mt; draw_md(); }, null); r.initial = false; if (server_ref != server_now) { console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|"); r.disabled = true; var msg = [ "The document has changed on the server.", "The changes will NOT be loaded into your editor automatically.", "", "Press F5 or CTRL-R to refresh the page,", "replacing your document with the server copy.", "", "You can close this message to ignore and contnue." ]; return toast.warn(0, msg.join('\n')); } console.log('modpoll eq'); }; setTimeout(r.periodic, 300); if (md_opt.modpoll_freq > 0) setInterval(r.periodic, 1000 * md_opt.modpoll_freq); return r; } var modpoll = new Modpoll(); window.onbeforeunload = function (e) { if ((ebi("save").className + '').indexOf('disabled') >= 0) return; //nice (todo) e.preventDefault(); //ff e.returnValue = ''; //chrome }; // save handler function save(e) { ev(e); var save_btn = ebi("save"), save_cls = save_btn.className + ''; if (save_cls.indexOf('disabled') >= 0) return toast.inf(2, "no changes"); var force = (save_cls.indexOf('force-save') >= 0); function save2() { var txt = dom_src.value, fd = new FormData(); fd.append("act", "tput"); fd.append("lastmod", (force ? -1 : last_modified)); fd.append("body", txt); var url = (location + '').split('?')[0]; var xhr = new XHR(); xhr.open('POST', url, true); xhr.responseType = 'text'; xhr.onload = xhr.onerror = save_cb; xhr.btn = save_btn; xhr.txt = txt; modpoll.skip_one = true; // skip one iteration while we save xhr.send(fd); } if (!force) save2(); else modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () { toast.inf(3, 'aborted'); }); } function save_cb() { if (this.status !== 200) return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText)); var r; try { r = JSON.parse(this.responseText); } catch (ex) { return toast.err(0, 'Error! The file was likely NOT saved.\n\nFailed to parse reply from server:\n\n' + unpre(this.responseText)); } if (!r.ok) { if (!clgot(this.btn, 'force-save')) { clmod(this.btn, 'force-save', 1); var msg = [ 'This file has been modified since you started editing it!\n', 'if you really want to overwrite, press save again.\n', 'modified ' + ((r.now - r.lastmod) / 1000) + ' seconds ago,', ((r.lastmod - last_modified) / 1000) + ' sec after you opened it\n', last_modified + ' lastmod when you opened it,', r.lastmod + ' lastmod on the server now,', r.now + ' server time now,\n', ]; return toast.err(0, msg.join('\n')); } else return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText); } clmod(this.btn, 'force-save'); //alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512); run_savechk(r.lastmod, this.txt, this.btn, 0); } function run_savechk(lastmod, txt, btn, ntry) { // download the saved doc from the server and compare var url = (location + '').split('?')[0] + '?_=' + Date.now(); var xhr = new XHR(); xhr.open('GET', url, true); xhr.responseType = 'text'; xhr.onload = xhr.onerror = savechk_cb; xhr.lastmod = lastmod; xhr.txt = txt; xhr.btn = btn; xhr.ntry = ntry; xhr.send(); } function savechk_cb() { if (this.status !== 200) return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText)); var doc1 = this.txt.replace(/\r\n/g, "\n"); var doc2 = this.responseText.replace(/\r\n/g, "\n"); if (doc1 != doc2) { var that = this; if (that.ntry < 10) { // qnap funny, try a few more times setTimeout(function () { run_savechk(that.lastmod, that.txt, that.btn, that.ntry + 1) }, 100); return; } modal.alert( 'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' + 'Length: yours=' + doc1.length + ', server=' + doc2.length ); modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']'); modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']'); return; } last_modified = this.lastmod; server_md = this.txt; draw_md(); toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : '')); modpoll.disabled = false; } // firefox bug: initial selection offset isn't cleared properly through js var ff_clearsel = (function () { if (UA.indexOf(') Gecko/') === -1) return function () { } return function () { var txt = dom_src.value; var y = dom_src.scrollTop; dom_src.value = ''; dom_src.value = txt; dom_src.scrollTop = y; }; })(); // returns car/cdr (selection bounds) and n1/n2 (grown to full lines) function linebounds(just_car, greedy_growth) { var car = dom_src.selectionStart, cdr = dom_src.selectionEnd; if (just_car) cdr = car; var md = dom_src.value, n1 = Math.max(car, 0), n2 = Math.min(cdr, md.length - 1); if (greedy_growth !== true) { if (n1 < n2 && md[n1] == '\n') n1++; if (n1 < n2 && md[n2 - 1] == '\n') n2 -= 2; } n1 = md.lastIndexOf('\n', n1 - 1) + 1; n2 = md.indexOf('\n', n2); if (n2 < n1) n2 = md.length; return { "car": car, "cdr": cdr, "n1": n1, "n2": n2, "md": md } } // linebounds + the three textranges function getsel() { var s = linebounds(false); s.pre = s.md.substring(0, s.n1); s.sel = s.md.substring(s.n1, s.n2); s.post = s.md.substring(s.n2); return s; } // place modified getsel into markdown function setsel(s) { if (s.car != s.cdr) { s.car = s.pre.length; s.cdr = s.pre.length + s.sel.length; } dom_src.value = [s.pre, s.sel, s.post].join(''); dom_src.setSelectionRange(s.car, s.cdr, dom_src.selectionDirection); dom_src.oninput(); // support chrome: dom_src.blur(); dom_src.focus(); } // cut/copy current line function md_cut(cut) { var s = linebounds(); if (s.car != s.cdr) return; dom_src.setSelectionRange(s.n1, s.n2 + 1, 'forward'); setTimeout(function () { var i = cut ? s.n1 : s.car; dom_src.setSelectionRange(i, i, 'forward'); }, 1); } // indent/dedent function md_indent(dedent) { var s = getsel(), sel0 = s.sel; if (dedent) s.sel = s.sel.replace(/^ /, "").replace(/\n /g, "\n"); else s.sel = ' ' + s.sel.replace(/\n/g, '\n '); if (s.car == s.cdr) s.car = s.cdr += s.sel.length - sel0.length; setsel(s); } // header function md_header(dedent) { var s = getsel(), sel0 = s.sel; if (dedent) s.sel = s.sel.replace(/^#/, "").replace(/^ +/, ""); else s.sel = s.sel.replace(/^(#*) ?/, "#$1 "); if (s.car == s.cdr) s.car = s.cdr += s.sel.length - sel0.length; setsel(s); } // smart-home function md_home(shift) { var s = linebounds(false, true), ln = s.md.substring(s.n1, s.n2), dir = dom_src.selectionDirection, rev = dir === 'backward', p1 = rev ? s.car : s.cdr, p2 = rev ? s.cdr : s.car, home = 0, lf = ln.lastIndexOf('\n') + 1, re = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/; if (rev) home = s.n1 + re.exec(ln)[0].length; else home = s.n1 + lf + re.exec(ln.substring(lf))[0].length; p1 = (p1 !== home) ? home : (rev ? s.n1 : s.n1 + lf); if (!shift) p2 = p1; if (rev !== p1 < p2) dir = rev ? 'forward' : 'backward'; if (!shift) ff_clearsel(); dom_src.setSelectionRange(Math.min(p1, p2), Math.max(p1, p2), dir); } // autoindent function md_newline() { var s = linebounds(true), ln = s.md.substring(s.n1, s.n2), m1 = /^( *)([0-9]+)(\. +)/.exec(ln), m2 = /^[ \t]*[>+*-]{0,2}[ \t]/.exec(ln), drop = dom_src.selectionEnd - dom_src.selectionStart; var pre = m2 ? m2[0] : ''; if (m1 !== null) pre = m1[1] + (parseInt(m1[2]) + 1) + m1[3]; if (pre.length > s.car - s.n1) // in gutter, do nothing return true; s.pre = s.md.substring(0, s.car) + '\n' + pre; s.sel = ''; s.post = s.md.substring(s.car + drop); s.car = s.cdr = s.pre.length; setsel(s); return false; } // backspace function md_backspace() { var s = linebounds(true), o0 = dom_src.selectionStart, left = s.md.slice(s.n1, o0), m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(left); // if car is in whitespace area, do nothing if (/^\s*$/.test(left)) return true; // same if selection if (o0 != dom_src.selectionEnd) return true; // same if line is all-whitespace or non-markup var v = m[0].replace(/[^ ]/g, " "); if (v === m[0] || v.length !== left.length) return true; s.pre = s.md.substring(0, s.n1) + v; s.sel = ''; s.post = s.md.substring(s.car); s.car = s.cdr = s.pre.length; setsel(s); return false; } // paragraph jump function md_p_jump(down) { var txt = dom_src.value, ofs = dom_src.selectionStart; if (down) { while (txt[ofs] == '\n' && --ofs > 0); ofs = txt.indexOf("\n\n", ofs); if (ofs < 0) ofs = txt.length - 1; while (txt[ofs] == '\n' && ++ofs < txt.length - 1); } else { txt += '\n\n'; while (ofs > 1 && txt[ofs - 1] == '\n') ofs--; ofs = Math.max(0, txt.lastIndexOf("\n\n", ofs - 1)); while (txt[ofs] == '\n' && ++ofs < txt.length - 1); } dom_src.setSelectionRange(ofs, ofs, "none"); } function reLastIndexOf(txt, ptn, end) { var ofs = (typeof end !== 'undefined') ? end : txt.length; end = ofs; while (ofs >= 0) { var sub = txt.slice(ofs, end); if (ptn.test(sub)) return ofs; ofs--; } return -1; } // json formatter function fmt_json(e) { ev(e); try { fmt_json2(); } catch (ex) { return toast.err(7, 'json-format (CTRL-J) failed\n\n(hint: select the json you want to beautify/minify first)\n\n' + ex); } } function fmt_json2() { var txt = dom_src.value, o0 = txt.lastIndexOf('\n', dom_src.selectionStart - 1), o1 = txt.indexOf('\n', dom_src.selectionEnd); o0 = o0 + 1 ? o0 + 1 : 0; if (o1 < 0) o1 = txt.length; for (var a = 0; a < 9; a++) { if (has(['\r', '\n', ' '], txt.charAt(o0))) ++o0; if (has(['\r', '\n', ' '], txt.charAt(o1))) --o1; } var jt0 = txt.slice(o0, ++o1), jo = JSON.parse(jt0), jt = JSON.stringify(jo, null, jt0.indexOf('\n') + 1 ? 0 : 2); setsel({ "pre": txt.slice(0, o0), "sel": jt, "post": txt.slice(o1), "car": o0, "cdr": o0, }); } // table formatter function fmt_table(e) { ev(e); try { fmt_table2(); } catch (ex) { return toast.err(7, 'table-format (CTRL-K) failed:\n' + ex); } } function fmt_table2() { var txt = dom_src.value, ofs = dom_src.selectionStart, //o0 = txt.lastIndexOf('\n\n', ofs), //o1 = txt.indexOf('\n\n', ofs); o0 = reLastIndexOf(txt, /\n\s*\n/m, ofs), o1 = txt.slice(ofs).search(/\n\s*\n|\n\s*$/m); // note \s contains \n but its fine if (o0 < 0) o0 = 0; else { // seek past the hit var m = /\n\s*\n/m.exec(txt.slice(o0)); o0 += m[0].length; } o1 = o1 < 0 ? txt.length : o1 + ofs; var err = 'cannot format table due to ', tab = txt.slice(o0, o1).split(/\s*\n/), re_ind = /^\s*/, ind = tab[1].match(re_ind)[0], r0_ind = tab[0].slice(0, ind.length), lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'), rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'), re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/, re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/, ncols; // the second row defines the table, // need to process that first var tmp = tab[0]; tab[0] = tab[1]; tab[1] = tmp; for (var a = 0; a < tab.length; a++) { var row_name = (a == 1) ? 'header' : 'row#' + (a + 1); var ind2 = tab[a].match(re_ind)[0]; if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0] return toast.err(7, err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]); var t = tab[a].slice(ind.length); t = t.replace(re_lpipe, ""); t = t.replace(re_rpipe, ""); tab[a] = t.split(/\s*\|\s*/g); if (a == 0) ncols = tab[a].length; else if (ncols < tab[a].length) return toast.err(7, err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length); // if row has less columns than row2, fill them in while (tab[a].length < ncols) tab[a].push(''); } // aight now swap em back tmp = tab[0]; tab[0] = tab[1]; tab[1] = tmp; var re_align = /^ *(:?)-+(:?) *$/; var align = []; for (var col = 0; col < tab[1].length; col++) { var m = tab[1][col].match(re_align); if (!m) return toast.err(7, err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']'); if (m[2]) { if (m[1]) align.push('c'); else align.push('r'); } else align.push('l'); } var pad = []; var tmax = 0; for (var col = 0; col < ncols; col++) { var max = 0; for (var row = 0; row < tab.length; row++) if (row != 1) max = Math.max(max, tab[row][col].length); var s = ''; for (var n = 0; n < max; n++) s += ' '; pad.push(s); tmax = Math.max(max, tmax); } var dashes = ''; for (var a = 0; a < tmax; a++) dashes += '-'; var ret = []; for (var row = 0; row < tab.length; row++) { var ln = []; for (var col = 0; col < tab[row].length; col++) { var p = pad[col]; var s = tab[row][col]; if (align[col] == 'l') { s = (s + p).slice(0, p.length); } else if (align[col] == 'r') { s = (p + s).slice(-p.length); } else { var pt = p.length - s.length; var pl = p.slice(0, Math.floor(pt / 2)); var pr = p.slice(0, pt - pl.length); s = pl + s + pr; } if (row == 1) { if (align[col] == 'l') s = dashes.slice(0, p.length); else if (align[col] == 'r') s = dashes.slice(0, p.length - 1) + ':'; else s = ':' + dashes.slice(0, p.length - 2) + ':'; } ln.push(s); } ret.push(ind + '| ' + ln.join(' | ') + ' |'); } // restore any markup in the row0 gutter ret[0] = r0_ind + ret[0].slice(ind.length); ret = { "pre": txt.slice(0, o0), "sel": ret.join('\n'), "post": txt.slice(o1), "car": o0, "cdr": o0 }; setsel(ret); } // show unicode function mark_uni(e) { ev(e); dom_tbox.className = ''; var txt = dom_src.value, ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'), mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771"); if (txt == mod) return toast.inf(5, 'no results; no modifications were made'); dom_src.value = mod; } // iterate unicode function iter_uni(e) { ev(e); var txt = dom_src.value, ofs = dom_src.selectionDirection == "forward" ? dom_src.selectionEnd : dom_src.selectionStart, re = new RegExp('([^' + js_uni_whitelist + ']+)'), m = re.exec(txt.slice(ofs)); if (!m) return toast.inf(5, 'no more hits from cursor onwards'); ofs += m.index; dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward"); dom_src.oninput(); // support chrome: dom_src.blur(); dom_src.focus(); } // configure whitelist function cfg_uni(e) { ev(e); modal.prompt("unicode whitelist", esc_uni_whitelist, function (reply) { esc_uni_whitelist = reply; js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\''); }, null); } var set_lno = (function () { var t = null, pi = null, pv = null, lno = ebi('lno'); var poke = function () { clearTimeout(t); t = setTimeout(fire, 20); } var fire = function () { try { clearTimeout(t); var i = dom_src.selectionStart; if (i === pi) return; var lns = dom_src.value.slice(0, i).split('\n'), v = lns.length + ' : ' + lns.pop().length; if (v != pv) lno.innerHTML = v; pi = i; pv = v; } catch (e) { } } timer.add(fire); return poke; })(); // hotkeys / toolbar (function () { var keydown = function (e) { if (!e && window.event) { e = window.event; if (dev_fbw == 1) { toast.warn(10, 'hello from fallback code ;_;\ncheck console trace'); console.error('using window.event'); } } var k = (e.key || e.code) + '', editing = document.activeElement == dom_src; if (k.startsWith('Key')) k = k.slice(3); var kl = k.toLowerCase(); if (dbg_kbd) console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which); if (ctrl(e) && kl == "s") { save(); return false; } if (k == "Escape" || k == "Esc") { var d = ebi('helpclose'); if (d) d.click(); } if (editing) set_lno(); if (ctrl(e)) { if (kl == "e") { dom_nsbs.click(); return false; } if (!editing) return true; if (kl == "h") { md_header(e.shiftKey); return false; } if (kl == "z") { if (e.shiftKey) action_stack.redo(); else action_stack.undo(); return false; } if (kl == "y") { action_stack.redo(); return false; } if (kl == "j") { fmt_json(e.shiftKey); return false; } if (kl == "k") { fmt_table(); return false; } if (kl == "u") { iter_uni(); return false; } if (k == "ArrowUp" || k == "ArrowDown") { md_p_jump(k == "ArrowDown"); return false; } if (kl == "x" || kl == "c") { md_cut(kl == "x"); return true; //sic } } else { if (!editing) return true; if (k == "Tab") { md_indent(e.shiftKey); return false; } if (k == "Home") { md_home(e.shiftKey); return false; } if (!e.shiftKey && k.endsWith("Enter")) { return md_newline(); } if (!e.shiftKey && k == "Backspace") { return md_backspace(); } } } document.onkeydown = keydown; ebi('save').onclick = save; })(); ebi('tools').onclick = function (e) { ev(e); var is_open = dom_tbox.className != 'open'; dom_tbox.className = is_open ? 'open' : ''; }; ebi('help').onclick = function (e) { ev(e); dom_tbox.className = ''; var dom = ebi('helpbox'); var dtxt = dom.getElementsByTagName('textarea'); if (dtxt.length > 0) { var txt = dtxt[0].value; if (!convert_markdown(txt, dom)) dom.innerText = txt.split('## markdown')[0]; dom.innerHTML = 'close' + dom.innerHTML; } dom.style.display = 'block'; ebi('helpclose').onclick = function () { dom.style.display = 'none'; }; }; ebi('fmt_table').onclick = fmt_table; ebi('fmt_json').onclick = fmt_json; ebi('mark_uni').onclick = mark_uni; ebi('iter_uni').onclick = iter_uni; ebi('cfg_uni').onclick = cfg_uni; // blame steen action_stack = (function () { var hist = { un: [], re: [] }; var sched_cpos = 0; var sched_timer = null; var ignore = false; var ref = dom_src.value; var diff = function (from, to, cpos) { if (from === to) return null; var car = 0, max = Math.max(from.length, to.length); for (; car < max; car++) if (from[car] != to[car]) break; var p1 = from.length, p2 = to.length; while (p1 --> 0 && p2 --> 0) if (from[p1] != to[p2]) break; if (car > ++p1) car = p1; var txt = from.substring(car, p1) return { car: car, cdr: p2 + (car && 1), txt: txt, cpos: cpos }; } var undiff = function (from, change) { var t1 = from.substring(0, change.car), t2 = from.substring(change.cdr); return { txt: t1 + change.txt + t2, cpos: change.cpos }; } var apply = function (src, dst) { dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length); if (src.length === 0) return false; var patch = src.pop(), applied = undiff(ref, patch), cpos = patch.cpos - (patch.cdr - patch.car) + patch.txt.length, reverse = diff(ref, applied.txt, cpos); if (reverse === null) return false; dst.push(reverse); ref = applied.txt; ignore = true; // just some browsers dom_src.value = ref; dom_src.setSelectionRange(cpos, cpos); ignore = true; // all browsers dom_src.oninput(); return true; } var schedule_push = function () { if (ignore) { ignore = false; return; } hist.re = []; clearTimeout(sched_timer); sched_cpos = dom_src.selectionEnd; sched_timer = setTimeout(push, 500); } var undo = function () { if (hist.re.length == 0) { clearTimeout(sched_timer); push(); } return apply(hist.un, hist.re); } var redo = function () { return apply(hist.re, hist.un); } var push = function () { var newtxt = dom_src.value; var change = diff(ref, newtxt, sched_cpos); if (change !== null) hist.un.push(change); ref = newtxt; dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length); if (hist.un.length > 0) dbg(jcp(hist.un.slice(-1)[0])); if (hist.re.length > 0) dbg(jcp(hist.re.slice(-1)[0])); } return { undo: undo, redo: redo, push: schedule_push, _hist: hist, _ref: ref } })(); J_MD2 = 2; ================================================ FILE: copyparty/web/mde.css ================================================ html .editor-toolbar>button { margin-left: -1px; border: 1px solid rgba(0,0,0,0.1) } html .editor-toolbar>button+button { border-left: 1px solid rgba(0,0,0,0) } html .editor-toolbar>button:hover, html .editor-toolbar>button:active { box-shadow: 0 .1em .3em #999; z-index: 9 } html .editor-toolbar>button:active, html .editor-toolbar>button.active { border-color: rgba(0,0,0,0.4); background: #fc0 } html .editor-toolbar>i.separator { border-left: 1px solid #ccc; } html .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 } html { line-height: 1.5em; } html, body { margin: 0; padding: 0; min-height: 100%; font-family: sans-serif; font-family: var(--font-main), sans-serif; background: #f7f7f7; color: #333; } #toast { bottom: auto; top: 1.4em; } #repl { position: absolute; top: 0; right: .5em; border: none; color: inherit; background: none; text-decoration: none; } #mn { font-weight: normal; margin: 1.3em 0 .7em 1em; } #mn a { color: #444; margin: 0 0 0 -.2em; padding: 0 0 0 .4em; text-decoration: none; /* ie: */ border-bottom: .1em solid #777\9; margin-right: 1em\9; } #mn a:first-child { padding-left: .5em; } #mn a:last-child { padding-right: .5em; } #mn a:not(:last-child):after { content: ''; width: 1.05em; height: 1.05em; margin: -.2em .3em -.2em -.4em; display: inline-block; border: 1px solid rgba(0,0,0,0.2); border-width: .2em .2em 0 0; transform: rotate(45deg); } #mn a:hover { color: #000; text-decoration: underline; } html .editor-toolbar>button.disabled { opacity: .35; pointer-events: none; } html .editor-toolbar>button.save.force-save { background: #f97; } .CodeMirror { background: #f7f7f7; } /* darkmode */ html.z .mdo, html.z .CodeMirror { border-color: #222; } html.z, html.z body, html.z .CodeMirror { background: #222; color: #ccc; } html.z .CodeMirror-cursor { border-color: #fff; } html.z .CodeMirror-selected { box-shadow: 0 0 1px #0cf inset; } html.z .CodeMirror-selected, html.z .CodeMirror-selectedtext { border-radius: .1em; background: #246; color: #fff; } html.z #mn a { color: #ccc; } html.z #mn a:not(:last-child):after { border-color: rgba(255,255,255,0.3); } html.z .editor-toolbar { border-color: #2c2c2c; background: #1c1c1c; } html.z .editor-toolbar>i.separator { border-left: 1px solid #444; border-right: 1px solid #111; } html.z .editor-toolbar>button { margin-left: -1px; border: 1px solid rgba(255,255,255,0.1); color: #aaa; } html.z .editor-toolbar>button:hover { color: #333; } html.z .editor-toolbar>button.active { color: #333; border-color: #ec1; background: #c90; } html.z .editor-toolbar::after, html.z .editor-toolbar::before { background: none; } /* ui.css overrides */ .mdo { padding: 1em; background: #f7f7f7; } html.z .mdo { background: #1c1c1c; } ================================================ FILE: copyparty/web/mde.html ================================================ 📝 {{ title }} {{ html_head }}
          Loading
          if you're still reading this, check that javascript is allowed
          π {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/mde.js ================================================ "use strict"; var J_MDE = 1; var dom_wrap = ebi('mw'); var dom_nav = ebi('mn'); var dom_doc = ebi('m'); var dom_md = ebi('mt'); (function () { var n = location + ''; n = (n.slice(n.indexOf('//') + 2).split('?')[0] + '?v').split('/'); n[0] = 'top'; var loc = []; var nav = []; for (var a = 0; a < n.length; a++) { if (a > 0) loc.push(n[a]); var dec = uricom_dec(n[a].split('?')[0]).replace(/&/g, "&").replace(//g, ">"); nav.push('' + dec + ''); } dom_nav.innerHTML = nav.join(''); })(); var mde = (function () { var tbar = [ { name: "light", title: "light", className: "fa fa-lightbulb", action: lightswitch }, { name: "save", title: "save", className: "fa fa-save", action: save }, '|', 'bold', 'italic', 'strikethrough', 'heading', '|', 'code', 'quote', 'unordered-list', 'ordered-list', 'clean-block', '|', 'link', 'image', 'table', 'horizontal-rule', '|', 'preview', 'side-by-side', 'fullscreen', '|', 'undo', 'redo']; var mde = new EasyMDE({ autoDownloadFontAwesome: false, autofocus: true, spellChecker: false, renderingConfig: { markedOptions: { breaks: true, gfm: true } }, shortcuts: { "save": "Ctrl-S" }, insertTexts: ["[](", ")"], indentWithTabs: false, tabSize: 2, toolbar: tbar, previewClass: 'mdo', onToggleFullScreen: set_jumpto, }); md_changed(mde, true); mde.codemirror.on("change", function () { md_changed(mde); }); qsr('#ml'); return mde; })(); function set_jumpto() { QS('.editor-preview-side').onclick = jumpto; } function jumpto(ev) { var tgt = ev.target; var ln = null; while (tgt && !ln) { ln = tgt.getAttribute('data-ln'); tgt = tgt.parentElement; } var ln = parseInt(ln); console.log(ln); var cm = mde.codemirror; var y = cm.heightAtLine(ln - 1, 'local'); var y2 = cm.heightAtLine(ln, 'local'); cm.scrollTo(null, y + (y2 - y) - cm.getScrollInfo().clientHeight / 2); } function md_changed(mde, on_srv) { if (on_srv) window.md_saved = mde.value(); var md_now = mde.value(); var save_btn = QS('.editor-toolbar button.save'); clmod(save_btn, 'disabled', md_now == window.md_saved); set_jumpto(); } function save(mde) { var save_btn = QS('.editor-toolbar button.save'); if (clgot(save_btn, 'disabled')) return toast.inf(2, 'no changes'); var force = clgot(save_btn, 'force-save'); function save2() { var txt = mde.value(); var fd = new FormData(); fd.append("act", "tput"); fd.append("lastmod", (force ? -1 : last_modified)); fd.append("body", txt); var url = (location + '').split('?')[0]; var xhr = new XHR(); xhr.open('POST', url, true); xhr.responseType = 'text'; xhr.onload = xhr.onerror = save_cb; xhr.btn = save_btn; xhr.mde = mde; xhr.txt = txt; xhr.send(fd); } if (!force) save2(); else modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () { toast.inf(3, 'aborted'); }); } function save_cb() { if (this.status !== 200) return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText)); var r; try { r = JSON.parse(this.responseText); } catch (ex) { return toast.err(0, 'Error! The file was likely NOT saved.\n\nFailed to parse reply from server:\n\n' + unpre(this.responseText)); } if (!r.ok) { if (!clgot(this.btn, 'force-save')) { clmod(this.btn, 'force-save', 1); var msg = [ 'This file has been modified since you started editing it!\n', 'if you really want to overwrite, press save again.\n', 'modified ' + ((r.now - r.lastmod) / 1000) + ' seconds ago,', ((r.lastmod - last_modified) / 1000) + ' sec after you opened it\n', last_modified + ' lastmod when you opened it,', r.lastmod + ' lastmod on the server now,', r.now + ' server time now,\n', ]; return toast.err(0, msg.join('\n')); } else return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText); } clmod(this.btn, 'force-save'); //alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512); // download the saved doc from the server and compare var url = (location + '').split('?')[0] + '?_=' + Date.now(); var xhr = new XHR(); xhr.open('GET', url, true); xhr.responseType = 'text'; xhr.onload = xhr.onerror = save_chk; xhr.btn = this.save_btn; xhr.mde = this.mde; xhr.txt = this.txt; xhr.lastmod = r.lastmod; xhr.send(); } function save_chk() { if (this.status !== 200) return toast.err(0, 'Error! The file was NOT saved.\n\nError ' + this.status + ":\n" + unpre(this.responseText)); var doc1 = this.txt.replace(/\r\n/g, "\n"); var doc2 = this.responseText.replace(/\r\n/g, "\n"); if (doc1 != doc2) { modal.alert( 'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' + 'Length: yours=' + doc1.length + ', server=' + doc2.length ); modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']'); modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']'); return; } last_modified = this.lastmod; md_changed(this.mde, true); toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : '')); } J_MDE = 2; ================================================ FILE: copyparty/web/msg.html ================================================ {{ s_doctitle }} {{ html_head }}
          {%- if h1 %}

          {{ h1 }}

          {%- endif %} {%- if h2 %}

          {{ h2 }}

          {%- endif %} {%- if p %}

          {{ p }}

          {%- endif %} {%- if pre %}
          {{ pre }}
          {%- endif %} {%- if html %} {{ html }} {%- endif %} {%- if click %} {%- endif %}
          {%- if redir %} {%- endif %} {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/opds.xml ================================================ {%- for d in dirs %} {{ d.name | e }} {{ d.iso8601 }} {%- endfor %} {%- for f in files %} {{ f.name | e }} {{ f.iso8601 }} {%- if f.jpeg_thumb_href != None %} {%- endif %} {%- if f.jpeg_thumb_href_hires != None %} {%- endif %} {%- endfor %} ================================================ FILE: copyparty/web/opds_osd.xml ================================================ {{ longname | truncate(16) | e }} {{ longname | e }} ================================================ FILE: copyparty/web/rups.css ================================================ html { color: #333; background: #f7f7f7; font-family: sans-serif; font-family: var(--font-main), sans-serif; touch-action: manipulation; } #wrap { margin: 2em auto; padding: 0 1em 3em 1em; line-height: 2.3em; } a { color: #047; background: #fff; text-decoration: none; border-bottom: 1px solid #8ab; border-radius: .2em; padding: .2em .6em; margin: 0 .3em; } #wrap td a { margin: 0; line-height: 1em; display: inline-block; white-space: initial; font-family: var(--font-main), sans-serif; } #repl { border: none; background: none; color: inherit; padding: 0; position: fixed; bottom: .25em; left: .2em; } #wrap table { border-collapse: collapse; position: relative; margin-top: 2em; } #wrap th { top: -1px; position: sticky; background: #f7f7f7; } #wrap td { font-family: var(--font-mono), monospace, monospace; white-space: pre; /*date*/ overflow: hidden; /*ipv6*/ } #wrap th:first-child, #wrap td:first-child { text-align: right; } #wrap td, #wrap th { text-align: left; padding: .3em .6em; max-width: 30vw; } #wrap tr:hover td { background: #ddd; box-shadow: 0 -1px 0 rgba(128, 128, 128, 0.5) inset; } #wrap th:first-child, #wrap td:first-child { border-radius: .5em 0 0 .5em; } #wrap th:last-child, #wrap td:last-child { border-radius: 0 .5em .5em 0; } html.z { background: #222; color: #ccc; } html.bz { background: #11121d; color: #bbd; } html.z a { color: #fff; background: #057; border-color: #37a; } html.z input[type=text] { color: #ddd; background: #223; border: none; border-bottom: 1px solid #fc5; border-radius: .2em; padding: .2em .3em; } html.z #wrap th { background: #222; } html.bz #wrap th { background: #223; } html.z #wrap tr:hover td { background: #000; } ================================================ FILE: copyparty/web/rups.html ================================================ {{ s_doctitle }} {{ html_head }}
          refresh control-panel   Filter:  
          π {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/rups.js ================================================ "use strict"; var J_RUP = 1; function render() { var html = ['']; var ups = V.ups, now = V.now; ebi('filter').value = V.filter; ebi('hits').innerHTML = 'showing ' + ups.length + ' files'; for (var a = 0; a < ups.length; a++) { var f = ups[a], vsp = vsplit(f.vp.split('?')[0]), dn = esc(uricom_dec(vsp[0])), fn = esc(uricom_dec(vsp[1])), at = f.at, td = now - f.at, ts = !at ? '(?)' : unix2ui(at), sa = !at ? '(?)' : td > 60 ? shumantime(td) : (td + 's'), sz = ('' + f.sz).replace(/\B(?=(\d{3})+(?!\d))/g, " "); html.push(''); } if (!ups.length) { var t = V.filter ? ' matching the filter' : ''; html = ['']; } html.push('
          sizewhoipwhenagedirfile
          ' + sz + '' + (f.un || '') + '' + f.ip + '' + ts + '' + sa + '' + dn + '' + fn + '
          there are no uploads' + t + '
          '); ebi('tw').innerHTML = html.join('\n'); } render(); var ti; function ask(e) { ev(e); clearTimeout(ti); ebi('hits').innerHTML = 'Loading...'; var xhr = new XHR(), filter = unsmart(ebi('filter').value); hist_replace(get_evpath().split('?')[0] + '?ru&filter=' + uricom_enc(filter)); xhr.onload = xhr.onerror = function () { try { V = JSON.parse(this.responseText) } catch (ex) { ebi('tw').innerHTML = 'failed to decode server response as json:
          ' + esc(this.responseText) + '
          '; return; } render(); }; xhr.open('GET', SR + '/?ru&j&filter=' + uricom_enc(filter), true); xhr.send(); } ebi('re').onclick = ask; ebi('filter').oninput = function () { clearTimeout(ti); ti = setTimeout(ask, 500); ebi('hits').innerHTML = '...'; }; ebi('filter').onkeydown = function (e) { if (('' + e.key).endsWith('Enter')) ask(); }; J_RUP = 2; ================================================ FILE: copyparty/web/shares.css ================================================ html { color: #333; background: #f7f7f7; font-family: sans-serif; font-family: var(--font-main), sans-serif; touch-action: manipulation; } #wrap { margin: 2em auto; padding: 0 1em 3em 1em; line-height: 2.3em; } #wrap>span { margin: 0 0 0 1em; border-bottom: 1px solid #999; } li { margin: 1em 0; } a { color: #047; background: #fff; text-decoration: none; white-space: nowrap; border-bottom: 1px solid #8ab; border-radius: .2em; padding: .2em .6em; margin: 0 .3em; } #wrap td a { margin: 0; } #w { color: #fff; background: #940; border-color: #b70; } #repl { border: none; background: none; color: inherit; padding: 0; position: fixed; bottom: .25em; left: .2em; } #wrap table { border-collapse: collapse; position: relative; margin-top: 2em; } th { top: -1px; position: sticky; background: #f7f7f7; } #wrap td, #wrap th { padding: .3em .6em; text-align: left; vertical-align: top; white-space: nowrap; } #wrap td+td+td+td+td+td+td+td { font-family: var(--font-mono), monospace, monospace; } #wrap th:first-child, #wrap td:first-child { border-radius: .5em 0 0 .5em; } #wrap th:last-child, #wrap td:last-child { border-radius: 0 .5em .5em 0; } #wrap.terse td div { height: 2.3em; overflow-y: hidden; } html.z { background: #222; color: #ccc; } html.z a { color: #fff; background: #057; border-color: #37a; } html.z th { background: #222; } html.bz { color: #bbd; background: #11121d; } html.bz th { background: #223; } ================================================ FILE: copyparty/web/shares.html ================================================ {{ s_doctitle }} {{ html_head }}
          refresh files control-panel axs = perms (read,write,get) nf = numFiles (0=dir) min/hrs = time left {%- for k, pw, vp, pr, st, un, t0, t1 in rows %} {%- endfor %}
          sharekey delete pw source axs nf user created expires min hrs add time
          qr {{ k }} delete {{ "yes" if pw else "--" }} /{{ vp|e }} {{ pr }}
          {{ st }}
          {{ un|e }} {{ t0 }} {{ t1 }} {{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 60) | round(1) }} {{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}
          {%- if not rows %} (you don't have any active shares btw) {%- endif %}
          π {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/shares.js ================================================ "use strict"; var J_SHR = 1; var t = QSA('a[k]'); for (var a = 0; a < t.length; a++) t[a].onclick = rm; function rm() { var u = SR + '/?eshare=rm&skey=' + uricom_enc(this.getAttribute('k')), xhr = new XHR(); xhr.open('POST', u, true); xhr.onload = xhr.onerror = cb; xhr.send(); } function bump() { var k = this.closest('tr').querySelector('a[k]').getAttribute('k'), u = SR + '/?skey=' + uricom_enc(k) + '&eshare=' + this.value, xhr = new XHR(); xhr.open('POST', u, true); xhr.onload = xhr.onerror = cb; xhr.send(); } function cb() { if (this.status !== 200) return modal.alert('
          server error
          ' + esc(unpre(this.responseText))); location = '?shares'; } ebi('xpnd').onclick = function (e) { ev(e); clmod(ebi('wrap'), 'terse', 't'); }; function qr(e) { ev(e); var href = this.href, pw = this.closest('tr').cells[2].textContent; if (pw.indexOf('yes') < 0) return showqr(href); modal.prompt("if you want to bypass the password protection by\nembedding the password into the qr-code, then\ntype the password now, otherwise leave this empty", "", function (v) { if (v) href += "&pw=" + v; showqr(href); }); } function showqr(href) { var vhref = href.replace('?qr&', '?').replace('?qr', ''); modal.alert(esc(vhref) + ''); } (function() { var tab = ebi('tab').tBodies[0], tr = Array.prototype.slice.call(tab.rows, 0); var buf = []; for (var a = 0; a < tr.length; a++) { var td = tr[a].cells[0], sa = td.getElementsByTagName('a'), h0 = sa[0].href, h1 = sa[1].href; sa[0].onclick = qr; if (!h0.startsWith(h1)) { var a2 = mknod('a', '', sa[1].innerHTML); a2.href = h0.slice(0, -3); sa[1].innerHTML = 'LAN'; td.appendChild(a2); } for (var b = 7; b < 9; b++) buf.push(parseInt(tr[a].cells[b].innerHTML)); } var ibuf = 0; for (var a = 0; a < tr.length; a++) for (var b = 7; b < 9; b++) { var v = buf[ibuf++]; tr[a].cells[b].innerHTML = v ? unix2ui(v).replace(' ', ', ') : 'never'; } for (var a = 0; a < tr.length; a++) tr[a].cells[11].innerHTML = ' ' + ''; var btns = QSA('td button'), aa = btns.length; for (var a = 0; a < aa; a++) btns[a].onclick = bump; })(); J_SHR = 2; ================================================ FILE: copyparty/web/splash.css ================================================ html { color: #333; background: #f7f7f7; font-family: sans-serif; font-family: var(--font-main), sans-serif; touch-action: manipulation; } #wrap { max-width: 40em; margin: 2em auto; padding: 0 1em 3em 1em; line-height: 1.3em; } #wrap.w { max-width: 96%; } h1 { border-bottom: 1px solid #ccc; margin: 2em 0 .4em 0; padding: 0; line-height: 1em; font-weight: normal; } li { margin: 1em 0; } #lo, a { color: #047; background: #fff; text-decoration: none; white-space: nowrap; border-bottom: 1px solid #8ab; border-radius: .2em; padding: .2em .6em; margin: 0 .3em; } td a { margin: 0; } #wb, #w { color: #fff; background: #940; border-color: #b70; } .af, .logout { float: right; margin: -.2em 0 0 .8em; } #lo, .logout, a.r { color: #c04; border-color: #c7a; } a.g { color: #0a0; border-color: #3a0; box-shadow: 0 .3em 1em #4c0; } #repl, #pb a { border: none; background: none; color: inherit; padding: 0; } #repl { position: fixed; bottom: .25em; left: .2em; } #pb { opacity: .5; position: fixed; bottom: .25em; right: .3em; } #pb span { opacity: .6; } #pb a { margin: 0; } table { border-collapse: collapse; } .vols td, .vols th { padding: .3em .6em; text-align: left; white-space: nowrap; } .vols td:empty, .vols th:empty { padding: 0; } .vols img { margin: -4px 0; } .num { border-right: 1px solid #bbb; } .num td { padding: .1em .7em .1em 0; } .num td:first-child { text-align: right; } .cn { text-align: center; } .btns { margin: 1em 0; } .btns>a:first-child { margin-left: 0; } .agr br { display: none; } #lo, .agr a, .agr form { margin: 0 .5em 0 0; line-height: 4em; } .agr form, .agr input { display: inline; padding: 0; margin: 0; } #lo, .agr input { line-height: 1em; font-weight: normal; } #msg { margin: 3em 0; } #msg h1 { margin-bottom: 0; } #msg h1 + p { margin-top: .3em; text-align: right; } blockquote { margin: 0 0 1.6em .6em; padding: .7em 1em 0 1em; border-left: .3em solid rgba(128,128,128,0.5); border-radius: 0 0 0 .25em; } pre, code { color: #480; background: #fff; font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; border: 1px solid rgba(128,128,128,0.3); border-radius: .2em; padding: .15em .2em; } html.z pre, html.z code { color: #9e0; background: #000; background: rgba(0,16,0,0.2); } .os { line-height: 1.5em; } .sph { margin-top: 4em; } .sph code { margin-left: .3em; } pre b, code b { color: #000; font-weight: normal; text-shadow: 0 0 .2em #3f3; border-bottom: 1px solid #090; } html.z pre b, html.z code b { color: #fff; border-bottom: 1px solid #9f9; } html.z { background: #222; color: #ccc; } html.z h1 { border-color: #777; } html.z #lo, html.z a { color: #fff; background: #057; border-color: #37a; } html.z .logout, html.z #lo, html.z a.r { background: #804; border-color: #c28; } html.z a.g { background: #470; border-color: #af4; box-shadow: 0 .3em 1em #7d0; } form { line-height: 2.5em; } #x, input { color: #a50; background: #fff; border: 1px solid #a50; border-radius: .3em; padding: .25em .6em; margin: 0 .3em 0 0; font-size: 1em; } input::placeholder { font-size: 1.2em; font-style: italic; letter-spacing: .04em; opacity: 0.64; color: #930; } #x, html.z input { color: #fff; background: #626; border-color: #c2c; } html.z input::placeholder { color: #fff; } html.z .num { border-color: #777; } html.bz { color: #bbd; background: #11121d; } html.bz .vols img { filter: sepia(0.8) hue-rotate(180deg); } ================================================ FILE: copyparty/web/splash.html ================================================ {{ s_doctitle }} {{ html_head }}
          {%- if not in_shr %} refresh connect {%- if this.uname == '*' %}

          howdy stranger   (you're not logged in)

          {%- else %} {%- if this.args.idp_logout %} logout {%- else %} logout {%- endif %}

          welcome back, {{ this.uname|e }}

          {%- endif %} {%- endif %} {%- if msg %}
          {{ msg }}
          {%- endif %} {%- if ups %}

          incoming files:

          {%- for u in ups %} {%- endfor %}
          %speedetaidledirfile
          {{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[5]|e }}{{ u[6]|e }}
          {%- endif %} {%- if dls %}

          active downloads:

          {%- for u in dls %} {%- endfor %}
          %sentspeedetaidledirfile
          {{ u[0] }}{{ u[1] }}{{ u[2] }}{{ u[3] }}{{ u[4] }}{{ u[5] }}{{ u[7]|e }}{{ u[8] }}
          {%- endif %} {%- if avol %}

          admin panel:

          scanning{{ scanning }}
          hash-q{{ hashq }}
          tag-q{{ tagq }}
          mtp-q{{ mtpq }}
          db-act{{ dbwt }}
          {%- for mp in avol %} {%- if mp in vstate and vstate[mp] %} {%- endif %} {%- endfor %}
          volactionstatus
          {{ mp }}rescan{{ vstate[mp] }}
          {%- endif %} {%- if rvol %}

          you can browse:

            {%- for mp in rvol %}
          • {{ mp }}
          • {%- endfor %}
          {%- endif %} {%- if wvol %}

          you can upload to:

            {%- for mp in wvol %}
          • {{ mp }}
          • {%- endfor %}
          {%- endif %} {%- if in_shr %}

          unlock this share:

          {%- if ahttps %} switch to https {%- endif %}
          {%- else %} {%- if this.uname == '*' %}

          login for more:

          {%- else %}

          change account:

          {%- endif %}
          {%- if this.args.idp_login %} {%- endif %} {%- if this.args.ao_have_pw %}
          {%- if this.args.usernames %} {%- else %} {%- endif %} {%- if chpw %} change password {%- endif %} {%- if ahttps %} switch to https {%- endif %}
          {%- endif %}
          {%- endif %}

          other stuff:

          {%- if ahttps %} switch to https
          {%- endif %} show recent uploads
          {%- if this.uname != '*' and this.args.shr %} edit shares
          {%- endif %} {%- if this.uname in this.args.idp_adm_set %} view idp cache
          {%- endif %} reset client settings
          {%- if this.uname != '*' and not in_shr %}
          {%- endif %}
            {%- if k304 or k304vis %} {%- if k304 %}
          • disable k304 (currently enabled) {%- else %}
          • enable k304 (currently disabled) {%- endif %}
            enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), but it will also make things slower in general
          • {%- endif %} {%- if no304 or no304vis %} {%- if no304 %}
          • disable no304 (currently enabled) {%- else %}
          • enable no304 (currently disabled) {%- endif %}
          • {%- endif %}
          π {%- if not this.args.nb %} powered by copyparty {{ver}} {%- endif %} {%- if lang != "eng" %} {%- endif %} {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/splash.js ================================================ "use strict"; var J_SPL = 1; Ls.eng = { "splash": { "d2": "shows the state of all active threads", "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes$N$Nnote: any changes to global settings$Nrequire a full restart to take effect", "lo2": "ends the session on all browsers", "u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds", "v2": "use this server as a local HDD", "ta1": "fill in your new password first", "ta2": "repeat to confirm new password:", "ta3": "found a typo; please try again", "nop": "ERROR: Password cannot be blank", "nou": "ERROR: Username and/or password cannot be blank", } }; if (window.langmod) langmod(); var d = (Ls[lang] || Ls.eng).splash; if (Ls.eng && d !== Ls.eng.splash) for (var k in Ls.eng.splash) if (d[k] === undefined) d[k] = Ls.eng.splash[k]; d.wb = d.w; for (var k in (d || {})) { var f = k.slice(-1), i = k.slice(0, -1), o = QSA(i.startsWith('.') ? i : '#' + i); for (var a = 0; a < o.length; a++) if (f == 1) o[a].innerHTML = d[k]; else if (f == 2) o[a].setAttribute("tt", d[k]); else if (f == 3) o[a].setAttribute("value", d[k]); else if (f == 4) o[a].setAttribute("placeholder", " " + d[k]); } var o1 = ebi('lo'), o2 = ebi('un'); if (o1 && o2 && d.lo3) o1.setAttribute("value", d.lo3.format(o2.textContent)); try { if (is_idp > 1) { var z = ['#l+div', '#l', '#c']; for (var a = 0; a < z.length; a++) QS(z[a]).style.display = 'none'; } } catch (ex) { } tt.init(); var o = QS('input[name="uname"]') || QS('input[name="cppwd"]'); if (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight) o.focus(); o = ebi('u'); if (o && /[0-9]+$/.exec(o.innerHTML)) o.innerHTML = shumantime(o.innerHTML); o = ebi('uhash') if (o) o.value = '' + location.hash; if (/\&re=/.test('' + location)) ebi('a').className = 'af g'; (function() { if (!ebi('x')) return; var pwi = ebi('lp'); function redo(msg) { modal.alert(msg, function() { pwi.value = ''; pwi.focus(); }); } function mok(v) { if (v !== pwi.value) return redo(d.ta3); pwi.setAttribute('name', 'pw'); ebi('la').value = 'chpw'; ebi('lf').submit(); } function stars() { var m = ebi('modali'); function enstars(n) { setTimeout(function() { m.value = ''; }, n); } m.setAttribute('type', 'password'); enstars(17); enstars(32); enstars(69); } ebi('x').onclick = function (e) { ev(e); if (!pwi.value) return ebi('lm').innerHTML = d.ta1; modal.prompt(d.ta2, "y", mok, null, stars); }; })(); if (ebi('lf')) ebi('lf').onsubmit = function() { var un = ebi('lu'); if (ebi('lp').value && (!un || un.value)) return true; ebi('lm').innerHTML = un ? d.nou : d.nop; return false; }; if (ebi('lp')) ebi('lp').oninput = function() { ebi('lm').innerHTML = this.value.length <= 64 ? '' : 'ERROR: Password too long (max=64)'; }; J_SPL = 2; ================================================ FILE: copyparty/web/svcs.html ================================================ {{ s_doctitle }} {{ html_head }}

          browse files // control panel

          or choose your OS for cooler alternatives:

          make this server appear on your computer as a regular HDD!
          pick your favorite below (sorted by performance, best first) and lets 🎉

          placeholders: {% if accs %}{% if un %}{{ un }}=username, {{ unpw }}=username:password, {% endif %}{{ pw }}=password, {% endif %}W:=mountpoint {% if accs %}{% if un %}{{ un }}=username, {{ unpw }}=username:password, {% endif %}{{ pw }}=password, {% endif %}mp=mountpoint {% if accs %}use real password{% endif %} show qr

          {% if args.have_idp_hdrs %}

          WARNING: this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see the IdP docs

          {%- endif %} {% if not args.no_dav %}

          WebDAV

          if you can, install winfsp+rclone and then paste this in cmd:

                          rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass={{ pw }}{% endif %}
                          rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-dav:{{ rvp }} W:
                      
            {%- if s %}
          • running rclone mount on LAN (or just dont have valid certificates)? add --no-check-certificate
          • {%- endif %}
          • old version of rclone? replace all = with   (space)

          if you want to use the native WebDAV client in windows instead (slow and buggy), first run webdav-cfg.bat to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:

                          {%- if un %}
                          net use w: http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} {{ pw }} /user:{{ b_un }}{% endif %}
                          {%- else %}
                          net use w: http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:{{ pw }}{% endif %}
                          {%- endif %}
                      

          rclone (v1.63 or later) is recommended:

                          rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass={{ pw }}{% endif %}
                          rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} mp
                      
            {%- if s %}
          • running rclone mount on LAN (or just dont have valid certificates)? add --no-check-certificate
          • {%- endif %}
          • running rclone mount as root? add --allow-other
          • old version of rclone? replace all = with   (space)

          alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):

                          yum install davfs2
                          {%- if un %}
                          {% if accs %}printf '%s\n' {{ b_un }} {{ pw }} | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} mp
                          {%- else %}
                          {% if accs %}printf '%s\n' {{ pw }} k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} mp
                          {%- endif %}
                      
          {%- if accs %}

          make davfs2 automount on boot:

                          {%- if un %}
                          printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} {{ pw }}" >> /etc/davfs2/secrets
                          {%- else %}
                          printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ pw }} k" >> /etc/davfs2/secrets
                          {%- endif %}
                          printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} mp davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
                      
          {%- endif %}

          or the emergency alternative (gnome/gui-only):

                          {%- if accs %}
                          echo {{ pw }} | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}
                          {%- else %}
                          gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
                          {%- endif %}
                      

          on KDE Dolphin, use webdav{{ s }}://{{ ep }}/{{ rvp }}

                          osascript -e ' mount volume "http{{ s }}://{{ b_un }}:{{ pw }}@{{ ep }}/{{ rvp }}" '
                      

          or you can open up a Finder, press command-K and paste this instead:

                          http{{ s }}://{{ b_un }}:{{ pw }}@{{ ep }}/{{ rvp }}
                      
          {%- if s %}

          replace https with http if it doesn't work

          {%- endif %}
          {%- endif %} {% if args.ftp or args.ftps %}

          FTP

          if you can, install winfsp+rclone and then paste this in cmd:

          {%- if args.ftp %}

          connect with plaintext FTP:

                          {%- if un %}
                          rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass={{ pw }}{% else %}anonymous pass=k{% endif %} tls=false
                          {%- else %}
                          rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}{{ pw }}{% else %}anonymous{% endif %} tls=false
                          {%- endif %}
                          rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftp:{{ rvp }} W:
                      
          {%- endif %} {%- if args.ftps %}

          connect with TLS-encrypted FTPS:

                          {%- if un %}
                          rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass={{ pw }}{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
                          {%- else %}
                          rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}{{ pw }}{% else %}anonymous{% endif %} tls=false explicit_tls=true
                          {%- endif %}
                          rclone mount --vfs-cache-mode writes --dir-cache-time 5s --network-mode {{ aname }}-ftps:{{ rvp }} W:
                      
          {%- endif %}
            {%- if args.ftps %}
          • running on LAN (or just dont have valid certificates)? add no_check_certificate=true to the config command
          • {%- endif %}
          • old version of rclone? replace all = with   (space)

          if you want to use the native FTP client in windows instead (please dont), press win+R and run this command:

                          {%- if un %}
                          explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:{{ pw }}@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
                          {%- else %}
                          explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ pw }}:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
                          {%- endif %}
                      
          {%- if args.ftp %}

          connect with plaintext FTP:

                          {%- if un %}
                          rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass={{ pw }}{% else %}anonymous pass=k{% endif %} tls=false
                          {%- else %}
                          rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}{{ pw }}{% else %}anonymous{% endif %} tls=false
                          {%- endif %}
                          rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} mp
                      
          {%- endif %} {%- if args.ftps %}

          connect with TLS-encrypted FTPS:

                          {%- if un %}
                          rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass={{ pw }}{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
                          {%- else %}
                          rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}{{ pw }}{% else %}anonymous{% endif %} tls=false explicit_tls=true
                          {%- endif %}
                          rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} mp
                      
          {%- endif %}
            {%- if args.ftps %}
          • running on LAN (or just dont have valid certificates)? add no_check_certificate=true to the config command
          • {%- endif %}
          • running rclone mount as root? add --allow-other
          • old version of rclone? replace all = with   (space)

          emergency alternative (gnome/gui-only):

                          {%- if accs %}
                          echo {{ pw }} | gio mount ftp{{ "" if args.ftp else "s" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
                          {%- else %}
                          gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
                          {%- endif %}
                      

          note: FTP is read-only on macos; please use WebDAV instead

                          open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:{{ pw }}@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
                      
          {%- endif %}

          partyfuse

          partyfuse.py -- fast, read-only, needs fuse.py in the same folder, needs winfsp doesn't need root

                      partyfuse.py{% if accs %} -a {{ unpw }}{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} W:mp
                  
          {%- if s %}
          • if you are on LAN (or just dont have valid certificates), add -td
          {%- endif %}

          you can use u2c.py to upload (sometimes faster than web-browsers)

          {% if args.smb %}

          SMB / CIFS

          {%- if un %}

          not available on this server because --usernames is enabled in the server config

          {%- else %}
                          net use w: \\{{ host }}\a{% if accs %} k /user:{{ pw }}{% endif %}
                      
                          mount -t cifs -o{% if accs %}user={{ pw }},pass=k,{% endif %}vers={{ 1 if args.smb1 else 2 }}.0,port={{ args.smb_port }},uid=1000 //{{ host }}/a/ mp
                      
                      open 'smb://{{ pw }}:k@{{ host }}/a'
                  
          {%- endif %} {%- endif %}

          ShareX

          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": "{{ unpw }}", {% 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": "{{ unpw }}", {% endif %}"accept": "url"
                          },
                          "DestinationType": "ImageUploader, TextUploader, FileUploader",
                          "FileFormName": "f" }
                      

          ishare

          to upload screenshots using ishare, save this as copyparty.iscu and run it:

                          { "Name": "copyparty",
                          "RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
                          "Headers": {
                              {%- if accs %}
                              "pw": "{{ unpw }}",
                              {%- endif %}
                              "accept": "json"
                          },
                          "ResponseURL": "{{ '{{fileurl}}' }}",
                          "FileFormName": "f" }
                      

          flameshot

          to upload screenshots using flameshot, save this as flameshot.sh and run it:

                          #!/bin/bash
                          pw="{{ unpw }}"
                          url="http{{ s }}://{{ ep }}/{{ rvp }}"
                          filename="$(date +%Y-%m%d-%H%M%S).png"
                          flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib
                      
          π {%- if js %} {%- endif %} ================================================ FILE: copyparty/web/svcs.js ================================================ "use strict"; var J_SVC = 1; var oa = QSA('pre'); for (var a = 0; a < oa.length; a++) { var html = oa[a].innerHTML, nd = /^ +/.exec(html)[0].length, rd = new RegExp('(^|\r?\n) {' + nd + '}', 'g'); oa[a].innerHTML = html.replace(rd, '$1').replace(/[ \r\n]+$/, '').replace(/\r?\n/g, '
          '); } function add_dls() { oa = QSA('pre.dl'); for (var a = 0; a < oa.length; a++) { var an = 'ta' + a, o = ebi(an) || mknod('a', an, 'download'); oa[a].setAttribute('id', 'tx' + a); oa[a].parentNode.insertBefore(o, oa[a]); o.setAttribute('download', oa[a].getAttribute('name')); o.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(oa[a].innerText)); clmod(o, 'txa', 1); } } add_dls(); oa = QSA('.ossel a'); for (var a = 0; a < oa.length; a++) oa[a].onclick = esetos; function esetos(e) { ev(e); setos(((e && e.target) || (window.event && window.event.srcElement)).id.slice(1)); } function setos(os) { var oa = QSA('.os'); for (var a = 0; a < oa.length; a++) oa[a].style.display = 'none'; var oa = QSA('.' + os); for (var a = 0; a < oa.length; a++) oa[a].style.display = ''; oa = QSA('.ossel a'); for (var a = 0; a < oa.length; a++) clmod(oa[a], 'g', oa[a].id.slice(1) == os); } setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk'); var un, un0, pw, pw0, unpw, up0; function setpw(e) { ev(e); if (!ebi('un0')) return askpw(); modal.prompt('username:', '', function (v) { if (!v) return; un = v; un0 = ebi('un0').innerHTML; var oa = QSA('b'); for (var a = 0; a < oa.length; a++) if (oa[a].innerHTML == un0) oa[a].textContent = un; askpw(); }); } function askpw() { modal.prompt('password:', '', function (v) { if (!v) return; pw = v; pw0 = ebi('pw0').innerHTML; var oa = QSA('b'); for (var a = 0; a < oa.length; a++) if (oa[a].innerHTML == pw0) oa[a].textContent = pw; if (un) { unpw = un ? (un+':'+pw) : pw; up0 = ebi('up0').innerHTML; for (var a = 0; a < oa.length; a++) if (oa[a].innerHTML == up0) oa[a].textContent = unpw; } add_dls(); }); } if (ebi('setpw')) ebi('setpw').onclick = setpw; ebi('qr').onclick = function () { var url = ('' + location).split('?')[0]; if (pw) url += '?pw=' + pw; var txt = esc(url) + ''; modal.alert(txt); }; J_SVC = 2; ================================================ FILE: copyparty/web/tl/chi.js ================================================ // 以 //m 结尾的行是未经验证的机器翻译 Ls.chi = { "tt": "中文", "cols": { "c": "操作按钮", "dur": "时长", "q": "质量 / 比特率", "Ac": "音频编码", "Vc": "视频编码", "Fmt": "格式 / 容器", "Ahash": "音频校验和", "Vhash": "视频校验和", "Res": "分辨率", "T": "文件类型", "aq": "音频质量 / 比特率", "vq": "视频质量 / 比特率", "pixfmt": "子采样 / 像素结构", "resw": "水平分辨率", "resh": "垂直分辨率", "chs": "声道数", "hz": "采样率", }, "hks": [ [ "杂项", ["ESC", "关闭各种窗口"], "文件管理器", ["G", "切换列表 / 网格视图"], ["T", "切换缩略图 / 图标"], ["⇧ A/D", "缩略图大小"], ["ctrl-K", "删除选中项"], ["ctrl-X", "剪切选中项到剪贴板"], ["ctrl-C", "复制选中项到剪贴板"], ["ctrl-V", "粘贴(移动/复制)到此处"], ["Y", "下载选中项"], ["F2", "重命名选中项"], "文件列表选择", ["space", "切换文件选择"], ["↑/↓", "移动选择光标"], ["ctrl ↑/↓", "移动光标和视图"], ["⇧ ↑/↓", "选择上一个/下一个文件"], ["ctrl-A", "选择所有文件 / 文件夹"] ], [ "导航", ["B", "切换面包屑导航 / 导航窗格"], ["I/K", "上一个/下一个文件夹"], ["M", "父文件夹(或折叠当前文件夹)"], ["V", "切换导航窗格显示文件夹 / 文本文件"], ["A/D", "导航窗格大小"] ], [ "音频播放器", ["J/L", "上一首/下一首歌曲"], ["U/O", "快退/快进 10 秒"], ["0..9", "跳转到 0%..90%"], ["P", "播放/暂停(或者启动播放)"], ["S", "选择正在播放的歌曲"], ["Y", "下载歌曲"] ], [ "图片查看器", ["J/L, ←/→", "上一张/下一张图片"], ["Home/End", "第一张/最后一张图片"], ["F", "全屏"], ["R", "顺时针旋转"], ["⇧ R", "逆时针旋转"], ["S", "选择图片"], ["Y", "下载图片"] ], [ "视频播放器", ["U/O", "快退/快进 10 秒"], ["P/K/Space", "播放/暂停"], ["C", "继续播放下一个视频"], ["V", "循环"], ["M", "静音"], ["[ 和 ]", "设置循环区间"] ], [ "文本文件查看器", ["I/K", "上一个/下一个文件"], ["M", "关闭文本文件"], ["E", "编辑文本文件"], ["S", "选择文件(用于剪切/复制/重命名)"], ["Y", "下载文本文件"], ["⇧ J", "美化 json"], ] ], "m_ok": "确定", "m_ng": "取消", "enable": "启用", "danger": "危险", "clipped": "已复制到剪贴板", "ht_s1": "秒", "ht_s2": "秒", "ht_m1": "分钟", "ht_m2": "分钟", "ht_h1": "小时", "ht_h2": "小时", "ht_d1": "天", "ht_d2": "天", "ht_and": "又 ", "goh": "控制面板", "gop": '上一个同级文件夹">前', "gou": '上一级文件夹">上', "gon": '下一个文件夹">后', "logout": "登出 ", "login": "登录", "access": " 权限", "ot_close": "关闭子菜单", "ot_search": "`按属性、路径/文件名、音乐标签或以上条件的任意组合搜索文件$N$N`foo bar` = 必须包含 «foo» 和 «bar»,$N`foo -bar` = 包含 «foo» 而不包含 «bar»,$N`^yana .opus$` = 以 «yama» 为开头的 «opus» 文件$N`"try unite"` = 精确包含 «try unite»$N$N时间格式为 iso-8601,例如:$N`2009-12-31`、`2020-09-12 23:30:00`", "ot_unpost": "取消发布:删除最近上传的内容,或中止未完成的上传", "ot_bup": "bup:基础上传器,连 netscape 4.0 都支持", "ot_mkdir": "mkdir:创建新目录", "ot_md": "new-file:创建新文本文件", "ot_msg": "msg:发送一条会输出到服务器日志中的消息", "ot_mp": "媒体播放器选项", "ot_cfg": "配置选项", "ot_u2i": 'up2k:上传文件(如果你有写入权限),或切换到搜索模式来判断文件是否存在于服务器上$N$N支持断点续传,多线程上传,保留文件时间戳,但比 [🎈] (基础上传器)更占 CPU

          上传过程中,此图标会变成进度条!', "ot_u2w": 'up2k:上传文件,支持断点续传(关闭浏览器后,再次上传同一文件即可续传)$N$N支持多线程上传,保留文件时间戳,但比 [🎈]  (基础上传器)更占 CPU

          上传过程中,此图标会变成进度条!', "ot_noie": '请使用 Chrome / Firefox / Edge', "ab_mkdir": "新建文件夹", "ab_mkdoc": "新建文本文件", "ab_msg": "发送消息到服务器日志", "ay_path": "跳转到文件夹", "ay_files": "跳转到文件", "wt_ren": "重命名选中的项目$N快捷键: F2", "wt_del": "删除选中的项目$N快捷键: ctrl-K", "wt_cut": "剪切选中的项目到剪贴板<small>(然后粘贴到其他地方)</small>$N快捷键: ctrl-X", "wt_cpy": "将选中的项目复制到剪贴板<small>(用于粘贴到其他地方)</small>$N快捷键: ctrl-C", "wt_pst": "粘贴之前剪切/复制的选择$N快捷键: ctrl-V", "wt_selall": "选择所有文件$N快捷键: ctrl-A(当焦点在文件上时)", "wt_selinv": "反选", "wt_zip1": "将此文件夹下载为压缩包", "wt_selzip": "将选中项下载为压缩包", "wt_seldl": "将选中项下载为单独的文件$N快捷键: Y", "wt_npirc": "复制 IRC 格式的曲目信息", "wt_nptxt": "复制纯文本格式的曲目信息", "wt_m3ua": "添加到 m3u 播放列表(加好后点击 📻copy)", "wt_m3uc": "复制 m3u 播放列表到剪贴板", "wt_grid": "切换网格/列表视图$N快捷键: G", "wt_prev": "上一曲$N快捷键: J", "wt_play": "播放/暂停$N快捷键: P", "wt_next": "下一曲$N快捷键: L", "ul_par": "并行上传:", "ut_rand": "随机化文件名", "ut_u2ts": "将最后修改的时间戳$N从你的文件系统复制到服务器\">📅", "ut_ow": "覆盖服务器上的现有文件?$N🛡️:不要覆盖(会生成新文件名)$N🕒:如果服务器文件较旧则覆盖$N♻️:只要文件内容不同就覆盖$N⏭️:无条件跳过所有已有文件", "ut_mt": "在上传时继续哈希其他文件$N$N如果你的 CPU 或硬盘是瓶颈,可能需要禁用", "ut_ask": '上传开始前询问确认">💭', "ut_pot": "通过简化界面来$N提高慢设备上的上传速度", "ut_srch": "不会真的上传,而是检查文件是否$N已经存在于服务器上(将扫描你可以读取的所有文件夹)", "ut_par": "设置为 0 可暂停上传$N$N如果你的网络很慢/延迟很高,请增加该值$N$N在局域网内/瓶颈在服务器硬盘时,请保持该值为 1", "ul_btn": "将文件/文件夹拖放到这里(或点击我)", "ul_btnu": "上 传", "ul_btns": "搜 索", "ul_hash": "哈希", "ul_send": "发送", "ul_done": "完成", "ul_idle1": "没有排队的上传任务", "ut_etah": "平均 <em>哈希</em> 速度和估计完成时间", "ut_etau": "平均 <em>上传</em> 速度和估计完成时间", "ut_etat": "平均 <em>总</em> 速度和估计完成时间", "uct_ok": "成功完成", "uct_ng": "有问题:失败/拒绝/未找到", "uct_done": "成功+失败", "uct_bz": "正在哈希或上传", "uct_q": "空闲,待处理", "utl_name": "文件名", "utl_ulist": "列出", "utl_ucopy": "复制", "utl_links": "链接", "utl_stat": "状态", "utl_prog": "进度", // 保持简短: "utl_404": "404", "utl_err": "错误", "utl_oserr": "OS错误", "utl_found": "已找到", "utl_defer": "延期", "utl_yolo": "YOLO", "utl_done": "完成", "ul_flagblk": "文件已添加到队列

          但别的标签页里已经有一个 up2k 实例在运行了,
          所以要先等待那边上传完成", "ul_btnlk": "服务器配置已将此开关锁定为当前状态", "udt_up": "上传", "udt_srch": "搜索", "udt_drop": "将文件拖放到这里", "u_nav_m": '
          好,你要传什么?
          Enter = 文件(不管有几个)\nESC = 一个文件夹(包括子文件夹)', "u_nav_b": '文件一个文件夹', "cl_opts": "开关选项", "cl_hfsz": "文件大小格式", "cl_themes": "主题", "cl_langs": "语言", "cl_ziptype": "打包下载", "cl_uopts": "up2k 开关", "cl_favico": "网站图标", "cl_bigdir": "大目录", "cl_hsort": "排序依据数", "cl_keytype": "调式记法", "cl_hiddenc": "隐藏列", "cl_hidec": "隐藏", "cl_reset": "重置", "cl_hpick": "在下方文件列表中点击某列表头即可从表中隐去该列", "cl_hcancel": "列隐藏操作已中止", "cl_rcm": "右键菜单", "ct_grid": '田 网格', "ct_ttips": '◔ ◡ ◔">ℹ️ 提示', "ct_thumb": '在网格视图中,切换图标或缩略图$N快捷键: T">🖼️ 缩略', "ct_csel": '在网格视图中,允许使用 CTRL 和 SHIFT 进行文件选择">选择', "ct_dsel": '在网格视图中,允许拖动选择">拖选', "ct_dl": '点击文件时强制下载(不要就地显示)">下载', "ct_ihop": '当图像查看器关闭时,滚动到刚才查看的文件">回跳⮯', "ct_dots": '显示隐藏文件(如果服务器允许)">显隐', "ct_qdel": '删除文件时,只需确认一次">快删', "ct_dir1st": '把文件夹排在文件之前">📁 在前', "ct_nsort": '按自然数顺序排列含有数字的文件名">数顺', "ct_utc": '按协调世界时显示所有时间">UTC', "ct_readme": '在文件夹列表中显示 README.md">📜 自述', "ct_idxh": '如有 index.html 则显示之,而非显示文件列表">网页', "ct_sbars": '显示滚动条">⟊', "cut_umod": "如果文件已存在于服务器上,将服务器的最后修改时间戳更新为你本地的时间戳(需要写入和删除权限)\">刷📅", "cut_turbo": "yolo 按钮,不作死就不会死,谨慎使用,后果自负!$N$N如果你上传大量文件到一半,由于某些原因不得不重新启动,$N然后想立刻开始继续上传,则可开启此“超频”(turbo)选项$N$N开启后,哈希校验将被简单的 “服务器上的文件大小是否相同?” 取代,$N因此内容不同但大小一致的文件将不会被上传$N$N你应该在上传完成后关闭此选项,$N然后重新“上传”同一批文件以供客户端校验\">超频", // turbo button 指 386 时代机箱上的降频按钮。此处 YOLO 选译为“作死”。 "cut_datechk": "只在启用超频(turbo)时有效$N$N略微降低 yolo / 作死程度:$N检查服务器上的文件时间戳是否与你的一致$N$N理论上 应该能发现大部分未上传完成/损坏的文件,$N但不能取代之后禁用超频再次校验\">戳检", "cut_u2sz": "每个上传块的大小(以 MiB 为单位);超远距离(如跨洋)传输用较大的值效果更好。网络非常不稳定时可尝试改小", "cut_flag": "确保一次只有一个标签页在上传$N——其他标签页也必须启用此选项$N——仅影响同一域名下的标签页", "cut_az": "按字母顺序上传文件,而不是按最小文件优先$N$N按字母顺序可以更容易地查看服务器上是否出现了问题,但在光纤/局域网上传稍微慢一些", "cut_nag": "上传完成时弹出操作系统通知$N(仅当浏览器或标签页在后台时)", "cut_sfx": "上传完成时发出声音提醒$N(仅当浏览器或标签页在后台时)", "cut_mt": "使用多线程加速文件哈希$N$N使用 web worker,占更多内存(最多多 512 MiB)$N$N能使上传速度在 https 下快 30%,http 下快 4.5 倍\">多线", "cut_wasm": "用 wasm 代替浏览器内置的哈希功能;可以加快 chrome 系浏览器上的速度,但会增加 CPU 占用,而且许多旧版本的 chrome 存在漏洞,启用此功能会导致浏览器耗光内存然后崩溃\">wasm", "cft_text": "网站图标文本(留空并刷新以禁用)", "cft_fg": "前景色", "cft_bg": "背景色", "cdt_lim": "文件夹中显示的最大文件数", "cdt_ask": "滚动到底部时,$N不会自动加载更多文件,$N而是询问要执行什么操作", "cdt_hsort": "`要包含在媒体 URL 中的排序依据(`,sorthref`)个数。设置为 0 时,也会忽略点击媒体链接时其中包含的排序规则", "cdt_ren": "启用自定义右键菜单,按住 shift 键并右键单击仍可打开浏览器菜单\">启用", "cdt_rdb": "当自定义右键菜单已打开并再次右键点击时显示浏览器右键菜单\">双击", "tt_entree": "显示导航面板(目录树侧边栏)$N快捷键: B", "tt_detree": "显示面包屑导航$N快捷键: B", "tt_visdir": "滚动到选定的文件夹", "tt_ftree": "切换文件夹树 / 文本文件$N快捷键: V", "tt_pdock": "用停靠在顶部的窗格显示父文件夹", "tt_dynt": "自动随着目录树展开而变宽", "tt_wrap": "自动换行", "tt_hover": "悬停时完整显示出写不下的文字$N(启用后,鼠标光标只有$N  位于左边线上才滚得动)", "ml_pmode": "文件夹播完后", "ml_btns": "命令", "ml_tcode": "转码", "ml_tcode2": "转为", "ml_tint": "不透明度", "ml_eq": "音频均衡器", "ml_drc": "动态范围压缩器", "ml_ss": "无声段自动快进", "mt_loop": "单曲循环\">🔁", "mt_one": "播完一首歌曲后停止\">1️⃣", "mt_shuf": "随机播放各文件夹中的歌曲\">🔀", "mt_aplay": "如果链接中有歌曲 ID,则自动播放$N$N禁用此选项将不再在播放音乐时更新页面 URL 中的歌曲 ID,以防止设置丢失但 URL 保留时又自动播放起来\">自▶", "mt_preload": "在歌曲快结束时开始加载下一首歌,以实现无缝播放\">预载", "mt_prescan": "在最后一首歌结束之前自动跳转到下一个文件夹$N以防止浏览器换页时停止播放\">预扫", "mt_fullpre": "尝试预加载整首歌;$N✅ 网络环境不稳定时启用,$N❌ 网络慢时大概应该禁用\">全载", "mt_fau": "在手机上,即使下一首歌预加载得不够快,也不要停止播放(可能导致标签显示异常)\">☕️", "mt_waves": "波形进度条:$N在进度条上显示音频振幅\">波形", "mt_npclip": "显示复制当前播放歌曲信息的按钮(IRC 格式或纯文本格式)\">/正播", "mt_m3u_c": "显示将所选歌曲复制为$Nm3u8 播放列表的按钮\">📻", "mt_octl": "启用操作系统集成(媒体快捷键 / osd)\">统控", "mt_oseek": "允许通过操作系统集成界面快进快退$N$N注意:在某些设备(如 iPhone)上,$N这会替换掉下一首按钮\">进退", "mt_oscv": "在系统 osd 中显示专辑封面\">封面", "mt_follow": "切换歌曲时,将正在播放的曲目滚动到视野内\">🎯", "mt_compact": "紧凑的控制按钮\">⟎", "mt_uncache": "清除缓存 $N(如果你的浏览器因缓存歌曲损坏而无法播放,请尝试此操作)\">清缓", "mt_mloop": "循环播放当前播放中的文件夹\">🔁 循环", "mt_mnext": "加载下一个文件夹并继续播放\">📂 继续", "mt_mstop": "停止播放\">⏸ 停止", "mt_cflac": "将 flac / wav 转换为 {0}\">flac", "mt_caac": "将 aac / m4a 转换为 {0}\">aac", "mt_coth": "将其他(非 mp3)文件转换为 {0}\">其他", "mt_c2opus": "适合桌面电脑、笔记本电脑和 android 设备的最佳选择\">opus", "mt_c2owa": "opus-weba(适用于 iOS 17.5 及更新版本)\">owa", "mt_c2caf": "opus-caf(适用于 iOS 11 到 17)\">caf", "mt_c2mp3": "适用于非常旧的设备\">mp3", "mt_c2flac": "音质最好,但很耗流量\">flac", "mt_c2wav": "无压缩播放(巨大无比)\">wav", "mt_c2ok": "不错的选择!", "mt_c2nd": "这不是你的设备推荐的输出格式,但应该没问题", "mt_c2ng": "你的设备似乎不支持此输出格式,不过还是先试试再说", "mt_xowa": "iOS 系统仍存在无法后台播放此格式的 bug,请改用 caf 或 mp3 格式", "mt_tint": "进度条未缓冲部分的不透明度(0-100)$N用以减轻缓冲造成的视觉干扰", "mt_eq": "`启用均衡器和增益控制;$N$Nboost `0` = 标准 100% 音量(无变化)$N$Nwidth `1  ` = 标准立体声(无变化)$Nwidth `0.5` = 50% 左右声道交叉馈送(crossfeed)$Nwidth `0  ` = 单声道$N$Nboost `-0.8` + width `10` = 消除人声 :^)$N$N启用均衡器可使无缝专辑完全无缝连播,所以如果你不要均衡但想要无缝,请保持均衡器开启,并将所有值设为零(除了 width = 1)", "mt_drc": "启用动态范围压缩器(音量整平 / 限幅器);会联动启用均衡器来均衡这团乱麻,如果你不要均衡,请将均衡器除“width”外的所有字段设置为 0$N$N降低 tresh dB 以上的音频的音量,超过 tresh dB 部分的每 ratio dB 会输出 1 dB,所以默认的 tresh = -24、ratio = 12 表示最终音量不会超过 -22 dB,可以放心将均衡器增益拉高到 0.8,甚至在起控时间(atk)为 0、释放时间(rls)超大(如 90)(仅在 Firefox 中有效,其他浏览器中 rls 最大为 1)的情况下可以提高到 1.8$N$N(请参考维基百科,比我讲得清楚多了)", "mt_ss": "`自动跳过无声部分:当音量低于 `音量`,且播放进度处于曲目前 `开头`% 或后 `结尾`% 时,以 `倍速` 倍速播放", "mt_ssvt": "音量阈值(0-255)\">音量", "mt_ssts": "进度阈值(在歌曲开头百分之多少启用此功能)\">开头", "mt_sste": "进度阈值(在歌曲结尾百分之多少启用此功能)\">结尾", "mt_ssrt": "音量/速度渐入/渐出用时\">渐变", "mt_sssm": "播放速度倍率(范围:0.15 到 8)\">倍速", "mb_play": "播放", "mm_hashplay": "播放这个音频文件?", "mm_m3u": "按 Enter/确定 播放\n按 ESC/取消 编辑", "mp_breq": "需要 Firefox 82+ 或 Chrome 73+ 或 iOS 15+", "mm_bload": "正在加载...", "mm_bconv": "正在转换为 {0},请稍等...", "mm_opusen": "你的浏览器无法播放 aac / m4a 文件,\n已启用转码为 opus", "mm_playerr": "播放失败:", "mm_eabrt": "播放的尝试已取消", "mm_enet": "你的网络连接不稳定", "mm_edec": "这个文件好像已损坏??", "mm_esupp": "你的浏览器不支持这个音频格式", "mm_eunk": "未知错误", "mm_e404": "无法播放音频;错误 404:文件未找到。", "mm_e403": "无法播放音频;错误 403:访问被拒绝。\n\n尝试按 F5 刷新,也许你已被注销", "mm_e415": "无法播放音频;错误 415:文件转码失败,请检查服务器日志。", "mm_e500": "无法播放音频;错误 500:请检查服务器日志。", "mm_e5xx": "无法播放音频;服务器错误", "mm_nof": "附近找不到更多音频文件", "mm_prescan": "正在寻找下一首音乐...", "mm_scank": "已找到下一首歌:", "mm_uncache": "缓存已清除,所有歌曲将在下次播放时重新下载", "mm_hnf": "那首歌已经没了", "im_hnf": "那张图片已经没了", "f_empty": '该文件夹为空', "f_chide": '即将隐藏 «{0}» 一列\n\n你可以在设置选项卡中恢复各列的显示', "f_bigtxt": "这个文件大小为 {0} MiB——真的要以文本形式查看吗?", "f_bigtxt2": "是否查看文件的结尾部分?这也将启用实时跟踪(tail)功能,实时显示新加的文本行。", "fbd_more": '
          已显示 {1} 个文件中的 {0} 个,显示 {2} 个显示全部
          ', "fbd_all": '
          已显示 {1} 个文件中的 {0} 个,显示全部
          ', "f_anota": "仅选择了 {1} 个项目中的 {0} 个;\n要选择整个文件夹,请先滚动到底", "f_dls": '当前文件夹中的文件链接已\n更改为下载链接', "f_dl_nd": '已跳过文件夹(请改去下载 zip/tar):\n', "f_partial": "要安全下载正在上传的文件,请点击没有 .PARTIAL 文件扩展名的同名文件。请按取消或 Escape 执行此操作。\n\n按 确定 / Enter 将忽略此警告并继续下载 .PARTIAL 临时文件,这几乎肯定会导致数据损坏。", "ft_paste": "粘贴 {0} 项$N快捷键: ctrl-V", "fr_eperm": '无法重命名:\n你在此文件夹中没有 “移动” 权限', "fd_eperm": '无法删除:\n你在此文件夹中没有 “删除” 权限', "fc_eperm": '无法剪切:\n你在此文件夹中没有 “移动” 权限', "fp_eperm": '无法粘贴:\n你在此文件夹中没有 “写入” 权限', "fr_emore": "请选择至少一个要重命名的项目", "fd_emore": "请选择至少一个要删除的项目", "fc_emore": "请选择至少一个要剪切的项目", "fcp_emore": "请选择至少一个要复制到剪贴板的项目", "fs_sc": "分享你所在的文件夹", "fs_ss": "分享选定的文件", "fs_just1d": "你不能同时选择多个文件夹,\n也不能同时选择文件夹和文件", "fs_abrt": "❌ 取消", "fs_rand": "🎲 随机名称", "fs_go": "✅ 创建分享链接", "fs_name": "名称", "fs_src": "源", "fs_pwd": "密码", "fs_exp": "过期", "fs_tmin": "分钟", "fs_thrs": "小时", "fs_tdays": "天", "fs_never": "永久", "fs_pname": "链接名称可留空,留空则随机生成", "fs_tsrc": "要分享的文件或文件夹", "fs_ppwd": "密码可留空", "fs_w8": "正在创建分享链接...", "fs_ok": "按 Enter/确定 复制到剪贴板\n按 ESC/取消 关闭", "frt_dec": "可能可以修复一些文件名损坏的情况\">url-decode", "frt_rst": "将修改后的文件名重置为原始文件名\">↺ 重置", "frt_abrt": "中止并关闭此窗口\">❌ 取消", "frb_apply": "应用重命名", "fr_adv": "批量 / 元数据 / 正则表达式重命名\">高级", "fr_case": "正则表达式区分大小写\">大小写", "fr_win": "兼容 Windows 文件名,将 <>:"\\|?* 替换为中文全角字符\">win", "fr_slash": "把 / 换掉,以免创建新文件夹\">不要 /", "fr_re": "`要在原始文件名上匹配的正则表达式;可在下面的 format 字段中按`(1)`、`(2)`的格式使用捕获组", "fr_fmt": "`借鉴 foobar2000:$N`(title)` 替换为歌曲名称,$N`[(artist) - ](title)` 当歌曲艺术家为空时省略[方括号]中的内容$N`$lpad((tn),2,0)` 将曲目编号补到 2 位数", "fr_pdel": "删除", "fr_pnew": "另存为", "fr_pname": "为你的新预设命名", "fr_aborted": "已中止", "fr_lold": "原文件名", "fr_lnew": "新文件名", "fr_tags": "选定文件的标签(只读,仅供参考):", "fr_busy": "正在重命名 {0} 个项目...\n\n{1}", "fr_efail": "重命名失败:\n", "fr_nchg": "{0} 个新文件名由于 win 和/或 不要 / 被更改\n\n确定继续使用这些有变的新文件名?", "fd_ok": "删除成功", "fd_err": "删除失败:\n", "fd_none": "没有文件被删除;可能被服务器配置(xbd)阻止?", "fd_busy": "正在删除 {0} 项...\n\n{1}", "fd_warn1": "删除这 {0} 项?", "fd_warn2": "最后一次确认! 无法撤销。要删除吗?", "fc_ok": "已剪切 {0} 个项目", "fc_warn": '已剪切 {0} 个项目\n\n但:只有当前浏览器标签页可以粘贴它们\n(因为选得实在太多了)', "fcc_ok": "已将 {0} 个项目复制到剪贴板", "fcc_warn": '已将 {0} 个项目复制到剪贴板\n\n但:只有当前浏览器标签页可以粘贴它们\n(因为选得实在太多了)', "fp_apply": "使用新文件名", "fp_skip": "跳过冲突项", "fp_ecut": "先剪切或复制一些文件/文件夹,然后才能粘贴/移动\n\n注:你可以在不同的浏览器标签页之间剪切/粘贴", "fp_ename": "{0} 个项目不能移动到这里,因为文件名已被占用。请在下方输入新名称以继续,或将名称留空(“跳过冲突项”)以跳过这些项目:", "fcp_ename": "{0} 个项目不能复制到这里,因为文件名已被占用。请在下方输入新名称以继续,或将名称留空(“跳过冲突项”)以跳过这些项目:", "fp_emore": "还有一些文件名冲突需要解决", "fp_ok": "移动成功", "fcp_ok": "复制成功", "fp_busy": "正在移动 {0} 项...\n\n{1}", "fcp_busy": "正在复制 {0} 项...\n\n{1}", "fp_abrt": "正在中止...", "fp_err": "移动失败:\n", "fcp_err": "复制失败:\n", "fp_confirm": "将这 {0} 项移动到这里?", "fcp_confirm": "将这 {0} 项复制到这里?", "fp_etab": '无法从其他浏览器标签页读取剪贴板', "fp_name": "从你的设备上传一个文件。给它一个名字:", "fp_both_m": '
          选择粘贴内容
          Enter = 从 «{1}» 移动 {0} 个文件\nESC = 从你的设备上传 {2} 个文件', "fcp_both_m": '
          选择粘贴内容
          Enter = 从 «{1}» 复制 {0} 个文件\nESC = 从你的设备上传 {2} 个文件', "fp_both_b": '移动上传', "fcp_both_b": '复制上传', "mk_noname": "请先在左侧文本框中输入文件名,再点击按钮 :p", "nmd_i1": "带上所需的文件扩展名,例如 .md", "nmd_i2": "由于你没有删除权限,只能创建 .{0} 文件", "tv_load": "加载文本文件:\n\n{0}\n\n{1}%(已加载 {2} MiB,共 {3} MiB)", "tv_xe1": "无法加载文本文件:\n\n错误 ", "tv_xe2": "404,找不到文件", "tv_lst": "文本文件列表", "tvt_close": "返回到文件夹视图$N快捷键: M(或 Esc)\">❌ 关闭", "tvt_dl": "下载此文件$N快捷键: Y\">💾 下载", "tvt_prev": "显示上一个文档$N快捷键: i\">⬆ 上一个", "tvt_next": "显示下一个文档$N快捷键: K\">⬇ 下一个", "tvt_sel": "选择文件 (用于剪切/删除/...)$N快捷键: S\">选择", "tvt_j": "美化 json$N快捷键: shift-J\">j", "tvt_edit": "在文本编辑器中打开文件$N快捷键: E\">✏️ 编辑", "tvt_tail": "监视文件更改,实时显示新行(tail)\">📡 实时", "tvt_wrap": "自动换行\">↵", "tvt_atail": "锁定滚动到页面底部\">⚓", "tvt_ctail": "解析终端颜色(ansi 转义码)\">🌈", "tvt_ntail": "滚动历史上限(保留多少字节的文本)", "m3u_add1": "歌曲已添加到 m3u 播放列表", "m3u_addn": "已添加 {0} 首歌曲到 m3u 播放列表", "m3u_clip": "m3u 播放列表已复制到剪贴板\n\n请创建一个名为 ××.m3u 的文本文件,并将播放列表粘贴进去,这样就可以播放了", "gt_vau": "不显示视频,仅播放音频\">🎧", "gt_msel": "启用文件选择;按住 ctrl 键可暂时恢复原点击功能$N$N<em>当启用时:双击可打开文件/文件夹</em>$N$N快捷键:S\">多选", "gt_crop": "截取缩略图中间一块\">裁剪", "gt_3x": "高分辨率缩略图\">3x", "gt_zoom": "缩放", "gt_chop": "截断", "gt_sort": "排序依据", "gt_name": "名称", "gt_sz": "大小", "gt_ts": "日期", "gt_ext": "类型", "gt_c1": "截掉更多文件名(显示部分更短)", "gt_c2": "截掉更少文件名(显示部分更长)", "sm_w8": "正在搜索...", "sm_prev": "上次查询的搜索结果:\n ", "sl_close": "关闭搜索结果", "sl_hits": "显示 {0} 个结果", "sl_moar": "加载更多", "s_sz": "大小", "s_dt": "日期", "s_rd": "路径", "s_fn": "名称", "s_ta": "标签", "s_ua": "上传于", "s_ad": "高级", "s_s1": "最小(MiB)", "s_s2": "最大(MiB)", "s_d1": "最早(iso8601 格式)", "s_d2": "最晚(iso8601 格式)", "s_u1": "上传时间不早于", "s_u2": "且/或不晚于", "s_r1": "路径包含  (空格分隔)", "s_f1": "名称包含  (用 -nope 反选)", "s_t1": "标签包含  (^=开头,结尾=$)", "s_a1": "特定元数据属性", "md_eshow": "无法渲染 ", "md_off": "[📜自述] 在 [⚙️] 中被禁用——文档已隐藏", "badreply": "解析服务器回复失败", "xhr403": "403:访问被拒绝\n\n尝试按 F5,也许你已被注销", "xhr0": "未知(可能已与服务器断开连接,或者服务器已离线)", "cf_ok": "抱歉——DD" + wah + "oS 防护启动\n\n应该会在大约 30 秒后恢复\n\n如果无事发生,按 F5 刷新页面", "tl_xe1": "无法列出子文件夹:\n\n错误 ", "tl_xe2": "404:找不到文件夹", "fl_xe1": "无法列出文件夹中的文件:\n\n错误 ", "fl_xe2": "404:找不到文件夹", "fd_xe1": "无法创建子文件夹:\n\n错误 ", "fd_xe2": "404:找不到父文件夹", "fsm_xe1": "无法发送消息:\n\n错误 ", "fsm_xe2": "404:找不到父文件夹", "fu_xe1": "无法从服务器加载取消发布列表:\n\n错误 ", "fu_xe2": "404:找不到文件??", "fz_tar": "未压缩的 gnu-tar 文件(linux / mac)", "fz_pax": "未压缩的 pax 格式 tar(较慢)", "fz_targz": "gnu-tar 格式,3 级 gzip 压缩$N$N通常非常慢,所以$N建议使用未压缩的 tar", "fz_tarxz": "gnu-tar 格式,1 级 xz 压缩$N$N通常非常慢,所以$N建议使用未压缩的 tar", "fz_zip8": "zip 格式,文件名用 utf8 编码(在 windows 7 及更早的系统上可能会出问题)", "fz_zipd": "zip 格式,文件名用传统 cp437 编码,适用于非常旧的软件", "fz_zipc": "文件名用 cp437 编码,预先计算 crc32,$N适用于 MS-DOS PKZIP v2.04g(1993 年 10 月)$N(下载开始前所需处理时间较长)", "un_m1": "你可以删除以下近期上传的文件(或中止未上传完的文件)", "un_upd": "刷新", "un_m4": "或分享下面列出的文件:", "un_ulist": "显示", "un_ucopy": "复制", "un_flt": "筛选:  URL 必须包含", "un_fclr": "取消筛选", "un_derr": '删除失败:\n', "un_f5": '出现问题,请尝试刷新或按 F5', "un_uf5": "抱歉,你必须先刷新页面(例如按 F5 或 CTRL-R),然后才能中止此上传", "un_nou": '警告: 服务器太繁忙,无法显示未上传完成的文件;请稍后点击“刷新”链接', "un_noc": '警告: 服务器配置中未启用/不允许取消发布已上传完成的文件', "un_max": "已显示前 2000 个文件(请使用筛选功能)", "un_avail": "{0} 个近期上传可以被删除
          {1} 个未完成的上传可以被中止", "un_m2": "按上传时间排序,最新的在前:", "un_no1": "哎呀!没有最近上传的文件", "un_no2": "哎呀!没有符合筛选条件的最近上传的文件", "un_next": "删除以下 {0} 个文件", "un_abrt": "中止", "un_del": "删除", "un_m3": "正在加载你最近上传的文件...", "un_busy": "正在删除 {0} 个文件...", "un_clip": "{0} 个链接已复制到剪贴板", "u_https1": "你应该", "u_https2": "切换到 https", "u_https3": "以获得更好的性能", "u_ancient": '你还在用远古浏览器,也许你应该 改用 bup', "u_nowork": "需要 Firefox 53+ 或 Chrome 57+ 或 iOS 11+", "tail_2old": "需要 Firefox 105+ 或 Chrome 71+ 或 iOS 14.5+", "u_nodrop": '浏览器版本低,不支持通过拖动文件到窗口来上传文件', "u_notdir": "这不是文件夹!\n\n你的浏览器太老了,\n请尝试将文件夹拖入窗口", "u_uri": "要从其他浏览器窗口拖放图片,\n请将其拖放到大上传按钮上", "u_enpot": '切换到 土豆界面(可能提高上传速度)', "u_depot": '切换到 精美界面(可能降低上传速度)', "u_gotpot": '切换到土豆界面以提高上传速度,\n\n不满意可以随时切换回去!', "u_pott": "

          文件:   {0} 个已完成,   {1} 个失败,   {2} 个正在处理,   {3} 个排队中

          ", "u_ever": "这是基本的上传工具,up2k 需要至少
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": '这是基本的上传工具,up2k 更好用', "u_uput": '提高速度(跳过校验和)', "u_ewrite": '你对这个文件夹没有写入权限', "u_eread": '你对这个文件夹没有读取权限', "u_enoi": '文件搜索在服务器配置中未启用', "u_enoow": "无法覆盖此处的文件;需要删除权限", "u_badf": '这 {0} 个文件(共 {1} 个)被跳过,可能是由于文件系统权限:\n\n', "u_blankf": '这 {0} 个文件(共 {1} 个)是空的,是否仍然上传?\n\n', "u_applef": "这 {0} 个文件(共 {1} 个)可能不是你想上传的。\n按 确定/Enter 跳过以下文件,\n按 取消/ESC 取消排除并上传这些文件:\n\n", "u_just1": '\n只选择一个文件的话也许能搞得好点', "u_ff_many": "如果你使用的是 Linux / MacOS / Android,这么多文件 可能 导致 Firefox 崩溃!\n如果发生这种情况,请再试一次(或换用 Chrome)。", "u_up_life": "这次上传的文件将在 {0}后从服务器删除", "u_asku": '将这 {0} 个文件上传到 {1}', "u_unpt": "你可以使用左上角的 🧯 撤销/删除此次上传", "u_bigtab": '即将显示 {0} 个文件,可能会导致你的浏览器崩溃。你确定吗?', "u_scan": '正在扫描文件...', "u_dirstuck": '目录迭代器尝试访问以下 {0} 个项目时卡住了,它们将被跳过:', "u_etadone": '已完成({0},{1} 个文件)', "u_etaprep": '(正在准备上传)', "u_hashdone": '哈希完成', "u_hashing": '哈希', "u_hs": '正在等待服务器...', "u_started": "文件现在正在上传,见 [🚀]", "u_dupdefer": "这是一个重复文件,将在其他所有文件上传完成后处理", "u_actx": "单击此文本以防止切换到其他窗口/选项卡时性能下降", "u_fixed": "好! 已修复 👍", "u_cuerr": "上传第 {0} 块(共 {1} 块)时失败,\n说不定没事,正在继续\n\n文件:{2}", "u_cuerr2": "服务器拒绝上传(第 {0} 块,共 {1} 块),\n稍后将重试\n\n文件:{2}\n\n错误 ", "u_ehstmp": "将重试,见右下角", "u_ehsfin": "服务器拒绝了上传结束的请求,正在重试...", "u_ehssrch": "服务器拒绝了执行搜索的请求,正在重试...", "u_ehsinit": "服务器拒绝了开始上传的请求,正在重试...", "u_eneths": "进行上传握手时发生网络错误,正在重试...", "u_enethd": "检测目标是否存在时发生网络错误,正在重试...", "u_cbusy": "已从网络问题中恢复,正在等待服务器认证...", "u_ehsdf": "服务器磁盘空间不足!\n\n仍将持续重试,万一有人\n清理出了足够空间就能继续", "u_emtleak1": "你的网页浏览器看起来可能有内存泄漏,\n请", "u_emtleak2": ' 切换到 https(推荐) 或 ', "u_emtleak3": ' ', "u_emtleakc": '尝试以下操作:\n
          • F5 刷新页面
          • 然后在 ⚙️ 设置 中禁用 多线 按钮
          • 然后再次尝试上传
          上传会稍微慢一些,不过没关系。\n麻烦你了!\n\nPS:chrome v107 已修复此问题', "u_emtleakf": '尝试以下操作:\n
          • F5 刷新页面
          • 然后在上传界面中启用 🥔(土豆)
          • 然后再次尝试上传
          \nPS: 希望 firefox 以后能修复此问题', "u_s404": "在服务器上未找到", "u_expl": "解释", "u_maxconn": "大多数浏览器限制为 6,但 Firefox 允许你在 about:config 中配置 connections-per-server 来提高上限", "u_tu": '

          警告:启用了超频(turbo), 客户端可能无法检测并恢复不完整的上传;请阅读超频按钮的帮助提示

          ', "u_ts": '

          警告:启用了超频(turbo), 搜索结果可能不正确;请阅读超频按钮的帮助提示

          ', "u_turbo_c": "服务器配置中禁用了超频(turbo)", "u_turbo_g": "超频(turbo)已禁用,因为你在此卷中没有\n列出目录下文件列表的权限", "u_life_cfg": '在 分钟(或 小时)后自动删除', "u_life_est": '上传的文件将于 --- 删除', "u_life_max": '此文件夹要求\n文件寿命不超过 {0}', "u_unp_ok": '{0}内允许取消发布', "u_unp_ng": '将不能取消发布', "ue_ro": '你对这个文件夹的权限是只读\n\n', "ue_nl": '你当前未登录', "ue_la": '你当前以 "{0}" 的身份登录', "ue_sr": '你当前处于文件搜索模式\n\n通过点击大搜索按钮旁边的放大镜 🔎 切换到上传模式,然后重试上传\n\n抱歉', "ue_ta": '尝试再次上传,现在应该可以了', "ue_ab": "这个文件正在上传到另一个文件夹,必须完成那次上传后,才能将此文件上传到其他地方。\n\n你可以通过左上角的 🧯 中止并丢弃那次上传", "ur_1uo": "成功:文件上传成功", "ur_auo": "成功:{0} 个文件全部上传成功", "ur_1so": "成功:文件已在服务器上找到", "ur_aso": "成功:{0} 个文件都已在服务器上找到", "ur_1un": "上传失败,抱歉", "ur_aun": "{0} 个文件全都上传失败了,抱歉", "ur_1sn": "文件未在服务器上找到", "ur_asn": "这 {0} 个文件未在服务器上找到", "ur_um": "完成;\n{0} 个上传成功,\n{1} 个上传失败,抱歉", "ur_sm": "完成;\n{0} 个文件在服务器上找到,\n{1} 个文件未在服务器上找到", "rc_opn": "打开", "rc_ply": "播放", "rc_pla": "作为音频播放", "rc_txt": "在文本文件查看器中打开", "rc_md": "在 markdown 查看器中打开", "rc_dl": "下载", "rc_zip": "下载为压缩包", "rc_cpl": "复制链接", "rc_del": "删除", "rc_cut": "剪切", "rc_cpy": "复制", "rc_pst": "粘贴", "rc_rnm": "重命名", "rc_nfo": "新建文件夹", "rc_nfi": "新建文件", "rc_sal": "全选", "rc_sin": "反选", "rc_shf": "共享此文件夹", "rc_shs": "共享选中项", "lang_set": "刷新以使更改生效?", "splash": { "a1": "刷新", "b1": "你好呀,陌生人   (你尚未登录)", "c1": "登出", "d1": "栈转储", "d2": "显示所有活动线程的状态", "e1": "重新加载配置", "e2": "重新加载配置文件(账户/卷/卷标),$N并重新扫描所有 e2ds 卷$N$N注意:任何全局设置的更改$N都需要完全重启才能生效", "f1": "你可以浏览:", "g1": "你可以上传到:", "cc1": "其他:", "h1": "禁用 k304", "i1": "启用 k304", "j1": "启用 k304 后,会在每次 HTTP 304 时断开客户端连接。这可解决某些有 bug 的代理服务器卡住的问题(突然加载不出页面), 也会降低各方面性能", "k1": "重置客户端设置", "l1": "登录看更多:", "ls3": "登录", "lu4": "用户名", "lp4": "密码", "lo3": "在所有浏览器中注销 “{0}”", "lo2": "将结束所有浏览器中的会话", "m1": "欢迎回来,", "n1": "404 找不到页面  ┐( ´ -`)┌", "o1": '或者你可能没有权限?尝试输入密码或 返回主页', "p1": "403:访问被拒绝  ~┻━┻", "q1": '尝试输入密码或 返回主页', "r1": "返回主页", ".s1": "重新扫描", "t1": "操作", "u2": "自上次服务器写入的时间$N( 上传 / 重命名 / ... )$N$N17d = 17 天$N1h23 = 1 小时 23 分钟$N4m56 = 4 分钟 56 秒", "v1": "连接", "v2": "将此服务器用作本地硬盘", "w1": "切换到 https", "x1": "更改密码", "y1": "编辑分享的文件", "z1": "解锁分享的文件:", "ta1": "请先输入新密码", "ta2": "重复以确认新密码:", "ta3": "打错了,请重试", "nop": "错误:密码不能为空", "nou": "错误:用户名和/或密码不能为空", "aa1": "正在接收的文件:", "ab1": "禁用 no304", "ac1": "启用 no304", "ad1": "启用 no304 将禁用所有缓存;如果 k304 没用,可以尝试此选项。这会浪费大量流量!", "ae1": "正在下载:", "af1": "显示最近上传的文件", "ag1": "查看 idp 缓存", } }; ================================================ FILE: copyparty/web/tl/cze.js ================================================ // Řádky končící na //m jsou neověřené strojové překlady Ls.cze = { "tt": "Čeština", "cols": { "c": "tlačítka akcí", "dur": "doba trvání", "q": "kvalita / bitrate", "Ac": "audio kodek", "Vc": "video kodek", "Fmt": "formát / kontejner", "Ahash": "kontrolní součet audia", "Vhash": "kontrolní součet videa", "Res": "rozlišení", "T": "typ souboru", "aq": "kvalita zvuku / bitrate", "vq": "kvalita videa / bitrate", "pixfmt": "podvzorkování / struktura pixelů", "resw": "horizontální rozlišení", "resh": "vertikální rozlišení", "chs": "audio kanály", "hz": "vzorkovací frekvence", }, "hks": [ [ "různé", ["ESC", "zavřít různé věci"], "správce souborů", ["G", "přepnout seznam / zobrazení mřížky"], ["T", "přepnout náhledy / ikony"], ["⇧ A/D", "velikost náhledů"], ["ctrl-K", "smazat vybrané"], ["ctrl-X", "vyjmout výběr do schránky"], ["ctrl-C", "kopírovat výběr do schránky"], ["ctrl-V", "vložit (přesunout/kopírovat) zde"], ["Y", "stáhnout vybrané"], ["F2", "přejmenovat vybrané"], "výběr souborů", ["space", "přepnout výběr souboru"], ["↑/↓", "posunout kurzor výběru"], ["ctrl ↑/↓", "posunout kurzor a zobrazení"], ["⇧ ↑/↓", "vybrat předchozí/následující soubor"], ["ctrl-A", "vybrat všechny soubory / složky"], ], [ "navigace", ["B", "přepnout drobečkovou navigaci / navigační panel"], ["I/K", "předchozí/následující složka"], ["M", "nadřazená složka (nebo sbalit aktuální)"], ["V", "přepnout složky / textové soubory v navigačním panelu"], ["A/D", "velikost navigačního panelu"], ], [ "audio přehrávač", ["J/L", "předchozí/následující skladba"], ["U/O", "přeskočit 10 sekund zpět/vpřed"], ["0..9", "přejít na 0%..90%"], ["P", "přehrát/pozastavit (také spustit)"], ["S", "vybrat přehrávanou skladbu"], ["Y", "stáhnout skladbu"], ], [ "prohlížeč obrázků", ["J/L, ←/→", "předchozí/následující obrázek"], ["Home/End", "první/poslední obrázek"], ["F", "celá obrazovka"], ["R", "otočit po směru hodinových ručiček"], ["⇧ R", "otočit proti směru hodinových ručiček"], ["S", "vybrat obrázek"], ["Y", "stáhnout obrázek"], ], [ "video přehrávač", ["U/O", "přeskočit 10 sekund zpět/vpřed"], ["P/K/Space", "přehrát/pozastavit"], ["C", "pokračovat přehráváním následující"], ["V", "smyčka"], ["M", "ztlumit"], ["[ and ]", "nastavit interval smyčky"], ], [ "prohlížeč textových souborů", ["I/K", "předchozí/následující soubor"], ["M", "zavřít textový soubor"], ["E", "upravit textový soubor"], ["S", "vybrat soubor (pro vyjmutí/kopírování/přejmenování)"], ["Y", "stáhnout textový soubor"], //m ["⇧ J", "zkrášlit json"], //m ] ], "m_ok": "OK", "m_ng": "Zrušit", "enable": "Povolit", "danger": "NEBEZPEČÍ", "clipped": "zkopírováno do schránky", "ht_s1": "sekunda", "ht_s2": "sekundy", "ht_s5": "sekund", "ht_m1": "minuta", "ht_m2": "minuty", "ht_m5": "minut", "ht_h1": "hodina", "ht_h2": "hodiny", "ht_h5": "hodin", "ht_d1": "den", "ht_d2": "dny", "ht_d5": "dní", "ht_and": " a ", "goh": "ovládací panel", "gop": 'předchozí sourozenec">předchozí', "gou": 'nadřazená složka">nahoru', "gon": 'následující složka">následující', "logout": "Odhlásit ", "login": "Přihlásit se", //m "access": " přístup", "ot_close": "zavřít podnabídku", "ot_search": "`hledat soubory podle atributů, cesty / názvu, hudebních tagů nebo jejich kombinace$N$N`foo bar` = musí obsahovat jak «foo» tak «bar»,$N`foo -bar` = musí obsahovat «foo» ale ne «bar»,$N`^yana .opus$` = začíná na «yana» a je to «opus» soubor$N`"try unite"` = obsahuje přesně «try unite»$N$Nformát data je iso-8601, jako$N`2009-12-31` nebo `2020-09-12 23:30:00`", "ot_unpost": "unpost: smazat vaše nedávné nahrání nebo zrušit nedokončené", "ot_bup": "bup: základní nahrávač, podporuje i netscape 4.0", "ot_mkdir": "mkdir: vytvořit nový adresář", "ot_md": "new-file: vytvořit nový textový soubor", //m "ot_msg": "msg: poslat zprávu do logu serveru", "ot_mp": "možnosti přehrávače médií", "ot_cfg": "možnosti konfigurace", "ot_u2i": 'up2k: nahrát soubory (pokud máte oprávnění k zápisu) nebo přepnout do vyhledávacího režimu a podívat se, zda existují někde na serveru$N$Nnahrávání je obnovitelné, vícevláknové a časové značky souborů jsou zachovány, ale používá více CPU než [🎈]  (základní nahrávač)

          během nahrávání se tato ikona stává indikátorem průběhu!', "ot_u2w": 'up2k: nahrát soubory s podporou obnovení (zavřete prohlížeč a stejné soubory přetáhněte později)$N$Nvícevláknové a časové značky souborů jsou zachovány, ale používá více CPU než [🎈]  (základní nahrávač)

          během nahrávání se tato ikona stává indikátorem průběhu!', "ot_noie": 'Prosím použijte Chrome / Firefox / Edge', "ab_mkdir": "vytvořit adresář", "ab_mkdoc": "nový textový soubor", //m "ab_msg": "poslat zprávu do logu serveru", "ay_path": "přejít na složky", "ay_files": "přejít na soubory", "wt_ren": "přejmenovat vybrané položky$NKlávesová zkratka: F2", "wt_del": "smazat vybrané položky$NKlávesová zkratka: ctrl-K", "wt_cut": "vyjmout vybrané položky <small>(pak je vložit někam jinam)</small>$NKlávesová zkratka: ctrl-X", "wt_cpy": "kopírovat vybrané položky do schránky$N(pro vložení někam jinam)$NKlávesová zkratka: ctrl-C", "wt_pst": "vložit dříve vyjmutý / zkopírovaný výběr$NKlávesová zkratka: ctrl-V", "wt_selall": "vybrat všechny soubory$NKlávesová zkratka: ctrl-A (když je zaměřen soubor)", "wt_selinv": "invertovat výběr", "wt_zip1": "stáhnout tuto složku jako archiv", "wt_selzip": "stáhnout výběr jako archiv", "wt_seldl": "stáhnout výběr jako samostatné soubory$NKlávesová zkratka: Y", "wt_npirc": "kopírovat informace o stopě ve formátu irc", "wt_nptxt": "kopírovat informace o stopě v prostém textu", "wt_m3ua": "přidat do m3u playlistu (klikněte později na 📻kopírovat)", "wt_m3uc": "kopírovat m3u playlist do schránky", "wt_grid": "přepnout zobrazení mřížky / seznamu$NKlávesová zkratka: G", "wt_prev": "předchozí stopa$NKlávesová zkratka: J", "wt_play": "přehrát / pozastavit$NKlávesová zkratka: P", "wt_next": "následující stopa$NKlávesová zkratka: L", "ul_par": "paralelní nahrávání:", "ut_rand": "náhodné názvy souborů", "ut_u2ts": "kopírovat časovou značku poslední změny$Nz vašeho souborového systému na server\">📅", "ut_ow": "přepsat existující soubory na serveru?$N🛡️: nikdy (místo toho vytvoří nový název souboru)$N🕒: přepsat pokud je soubor na serveru starší než váš$N♻️: vždy přepsat pokud se soubory liší$N⏭️: bezpodmínečně přeskočit všechny existující soubory", //m "ut_mt": "pokračovat v hashování ostatních souborů během nahrávání$N$Nmožná zakázat pokud je vaše CPU nebo HDD bottleneckem", "ut_ask": 'požádat o potvrzení před zahájením nahrávání">💭', "ut_pot": "zlepšit rychlost nahrávání na pomalých zařízeních$Nzjednodušením UI", "ut_srch": "skutečně nenahrávat, místo toho zkontrolovat zda soubory již $N existují na serveru (prohledá všechny složky které můžete číst)", "ut_par": "pozastavit nahrávání nastavením na 0$N$Nzvýšit pokud je vaše připojení pomalé / vysoká latence$N$Nponechat na 1 v LAN nebo pokud je HDD serveru bottleneckem", "ul_btn": "přetáhněte soubory / složky
          sem (nebo sem klikněte)", "ul_btnu": "N A H R Á T", "ul_btns": "H L E D A T", "ul_hash": "hash", "ul_send": "odeslat", "ul_done": "hotovo", "ul_idle1": "zatím nejsou zařazena žádná nahrávání", "ut_etah": "průměrná rychlost <em>hashování</em> a odhadovaný čas do dokončení", "ut_etau": "průměrná rychlost <em>nahrávání</em> a odhadovaný čas do dokončení", "ut_etat": "průměrná <em>celková</em> rychlost a odhadovaný čas do dokončení", "uct_ok": "úspěšně dokončeno", "uct_ng": "nedobré: selhalo / odmítnuto / nenalezeno", "uct_done": "celkem", "uct_bz": "hashování nebo nahrávání", "uct_q": "nečinné, čekající", "utl_name": "název souboru", "utl_ulist": "seznam", "utl_ucopy": "kopírovat", "utl_links": "odkazy", "utl_stat": "stav", "utl_prog": "průběh", // keep short: "utl_404": "404", "utl_err": "CHYBA", "utl_oserr": "chyba OS", "utl_found": "nalezeno", "utl_defer": "odložit", "utl_yolo": "YOLO", "utl_done": "hotovo", "ul_flagblk": "soubory byly přidány do fronty
          nicméně v jiné kartě prohlížeče běží up2k,
          takže čekáme až skončí", "ul_btnlk": "konfigurace serveru uzamkla tento přepínač v tomto stavu", "udt_up": "Nahrávání", "udt_srch": "Hledání", "udt_drop": "přetáhněte to sem", "u_nav_m": '
          jasnačka, co máte?
          Enter = Soubory (jeden nebo více)\nESC = Jednu složku (včetně podsložek)', "u_nav_b": 'SouboryJedna složka', "cl_opts": "přepínače", "cl_hfsz": "velikost souboru", //m "cl_themes": "téma", "cl_langs": "jazyk", "cl_ziptype": "stahování složky", "cl_uopts": "up2k přepínače", "cl_favico": "favicon", "cl_bigdir": "velké adresáře", "cl_hsort": "#řazení", "cl_keytype": "notace kláves", "cl_hiddenc": "skryté sloupce", "cl_hidec": "skrýt", "cl_reset": "resetovat", "cl_hpick": "klepněte na záhlaví sloupců pro skrytí v tabulce níže", "cl_hcancel": "skrývání sloupců zrušeno", "cl_rcm": "kontextová nabídka", //m "ct_grid": '田 mřížka', "ct_ttips": '◔ ◡ ◔">ℹ️ nápovědy', "ct_thumb": 'v zobrazení mřížky přepnout ikony nebo náhledy$NKlávesová zkratka: T">🖼️ náhledy', "ct_csel": 'použít CTRL a SHIFT pro výběr souborů v zobrazení mřížky">výběr', "ct_dsel": 'použít tažený výběr v zobrazení mřížky">tažení', //m "ct_dl": 'vynutit stažení (nezobrazovat inline) při kliknutí na soubor">dl', //m "ct_ihop": 'když se zavře prohlížeč obrázků, posunout dolů k naposledy zobrazenému souboru">g⮯', "ct_dots": 'zobrazit skryté soubory (pokud to server povoluje)">dotfiles', "ct_qdel": 'při mazání souborů požádat o potvrzení jen jednou">rychlé mazání', "ct_dir1st": 'řadit složky před soubory">📁 první', "ct_nsort": 'přirozené řazení (pro názvy souborů s úvodními číslicemi)">přirozené řazení', "ct_utc": 'zobrazit všechny časy v UTC">UTC', "ct_readme": 'zobrazit README.md v seznamech složek">📜 readme', "ct_idxh": 'zobrazit index.html místo seznamu složky">htm', "ct_sbars": 'zobrazit posuvníky">⟊', "cut_umod": "pokud soubor na serveru již existuje, aktualizovat časovou značku posledního změny serveru tak, aby odpovídala vašemu lokálnímu souboru (vyžaduje oprávnění k zápisu+mazání)\">re📅", "cut_turbo": "yolo tlačítko, pravděpodobně to NECHCETE povolit:$N$Npoužijte to pokud jste nahrávali obrovské množství souborů a museli jste restartovat z nějakého důvodu a chcete pokračovat v nahrávání ASAP$N$Ntoto nahradí hash-kontrolu jednoduchým "má to stejnou velikost souboru na serveru?" takže pokud se obsah souborů liší, NEBUDE nahrán$N$Nměli byste to vypnout když nahrávání skončí a pak znovu "nahrát" stejné soubory aby je klient ověřil\">turbo", "cut_datechk": "nemá žádný efekt pokud není povoleno turbo tlačítko$N$Nsnižuje yolo faktor o trochu; kontroluje zda časové značky souborů na serveru odpovídají vašim$N$Nměl by teoreticky zachytit většinu nedokončených / poškozených nahrávání, ale není náhradou za ověřovací průchod s turbem vypnutým poté\">kontrola data", "cut_u2sz": "velikost (v MiB) každého kusu nahrávání; velké hodnoty lépe létají přes atlantik. Zkuste nízké hodnoty na velmi nespolehlivých připojeních", "cut_flag": "zajistit aby nahrávala jen jedna karta najednou $N -- ostatní karty to musí mít také povoleno $N -- ovlivňuje jen karty na stejné doméně", "cut_az": "nahrávat soubory v abecedním pořadí, spíše než nejmenší-soubor-první$N$Nabecední pořadí může usnadnit kontrolu zda se něco pokazilo na serveru, ale činí nahrávání mírně pomalejší na optice / LAN", "cut_nag": "notifikace OS když nahrávání skončí$N(jen pokud prohlížeč nebo karta není aktivní)", "cut_sfx": "zvukové upozornění když nahrávání skončí$N(jen pokud prohlížeč nebo karta není aktivní)", "cut_mt": "použít vícevláknové zpracování pro zrychlení hashování souborů$N$Ntoto používá web-workers a vyžaduje$Nvíce RAM (až 512 MiB navíc)$N$Ndělá https o 30% rychlejší a http 4,5x rychlejší\">mt", "cut_wasm": "použijte wasm místo vestavěného hashování prohlížeče; zlepšuje rychlost na prohlížečích založených na chrome ale zvyšuje zátěž CPU, mnoho starších verzí chrome má chyby které způsobují že prohlížeč spotřebuje veškerou RAM a spadne pokud je toto povoleno\">wasm", "cft_text": "text favicon (prázdné a obnovte pro zakázání)", "cft_fg": "barva popředí", "cft_bg": "barva pozadí", "cdt_lim": "maximální počet souborů k zobrazení ve složce", "cdt_ask": "při posunování na konec,$Nmísto načítání více souborů,$N se zeptat co dělat", "cdt_hsort": "`kolik pravidel řazení (`,sorthref`) zahrnout do media-URL. Nastavení na 0 bude také ignorovat pravidla řazení zahrnutá v media odkazech při kliknutí na ně", "cdt_ren": "povolit vlastní kontextovou nabídku, běžnou nabídku lze otevřít podržením klávesy shift a kliknutím pravým tlačítkem\">povolit", //m "cdt_rdb": "zobrazit běžné menu pravého tlačítka, když je vlastní již otevřené a znovu se klikne pravým\">x2", //m "tt_entree": "zobrazit navigační panel (postranní strom adresářů)$NKlávesová zkratka: B", "tt_detree": "zobrazit drobečkovou navigaci$NKlávesová zkratka: B", "tt_visdir": "posunout k vybrané složce", "tt_ftree": "přepnout strom složek / textové soubory$NKlávesová zkratka: V", "tt_pdock": "zobrazit nadřazené složky v ukotveném panelu nahoře", "tt_dynt": "automaticky rozrůstat jak se strom rozšiřuje", "tt_wrap": "zalomení řádků", "tt_hover": "odhalit přetékající řádky při najetí$N( ruší posun pokud kurzor myši $N  není v levém okraji )", "ml_pmode": "na konci složky...", "ml_btns": "příkazy", "ml_tcode": "transkódovat", "ml_tcode2": "transkódovat na", "ml_tint": "odstín", "ml_eq": "audio ekvalizér", "ml_drc": "kompresor dynamického rozsahu", "ml_ss": "přeskočit ticho", //m "mt_loop": "smyčka/opakovat jednu skladbu\">🔁", "mt_one": "zastavit po jedné skladbě\">1️⃣", "mt_shuf": "zamíchat skladby v každé složce\">🔀", "mt_aplay": "automatické přehrávání pokud je ID skladby v odkazu kterým jste přišli na server$N$Nzakázání toho také zastaví aktualizaci URL stránky s ID skladby při přehrávání hudby, aby se zabránilo automatickému přehrávání pokud se tato nastavení ztratí ale URL zůstane\">a▶", "mt_preload": "začít načítat následující skladbu před koncem pro plynulé přehrávání\">přednahrání", "mt_prescan": "přejít do následující složky před tím než$Nskončí poslední skladba, aby byl webprohlížeč$Nspokojen aby nezastavil přehrávání\">nav", "mt_fullpre": "zkusit přednahrát celou skladbu;$N✅ povolit na nespolehlivých připojeních,$N❌ zakázat na pomalých připojeních pravděpodobně\">úplné", "mt_fau": "na telefonech zabránit zastavení hudby, pokud se další píseň nenahraje dostatečně rychle (může způsobit chybné zobrazení tagů)\">☕️", "mt_waves": "vlnový posuvník:$Nzobrazit amplitudu zvuku v posuvníku\">~s", "mt_npclip": "zobrazit tlačítka pro kopírování aktuálně přehrávané písně do schránky\">/np", "mt_m3u_c": "zobrazit tlačítka pro kopírování$Nvybraných písní jako položky m3u8 playlistu\">📻", "mt_octl": "integrace s OS (mediální klávesy / osd)\">os-ctl", "mt_oseek": "povolit posunování přes integraci s OS$N$Npoznámka: na některých zařízeních (iPhone),$Nto nahradí tlačítko další písně\">seek", "mt_oscv": "zobrazit obal alba v osd\">art", "mt_follow": "udržet přehrávanou stopu v zobrazení\">🎯", "mt_compact": "kompaktní ovládání\">⟎", "mt_uncache": "vymazat cache  (zkuste to, pokud váš prohlížeč uložil$Nporušenou kopii písně a odmítá ji přehrát)\">uncache", "mt_mloop": "opakovat otevřenou složku\">🔁 loop", "mt_mnext": "načíst další složku a pokračovat\">📂 next", "mt_mstop": "zastavit přehrávání\">⏸ stop", "mt_cflac": "převést flac / wav na {0}\">flac", "mt_caac": "převést aac / m4a na {0}\">aac", "mt_coth": "převést všechny ostatní (ne mp3) na {0}\">oth", "mt_c2opus": "nejlepší volba pro desktopy, laptopy, android\">opus", "mt_c2owa": "opus-weba, pro iOS 17.5 a novější\">owa", "mt_c2caf": "opus-caf, pro iOS 11 až 17\">caf", "mt_c2mp3": "použijte na velmi starých zařízeních\">mp3", "mt_c2flac": "nejlepší kvalita zvuku, ale obrovské stahování\">flac", "mt_c2wav": "nekomprimované přehrávání (ještě větší)\">wav", "mt_c2ok": "výborně, dobrá volba", "mt_c2nd": "to není doporučený výstupní formát pro vaše zařízení, ale v pořádku", "mt_c2ng": "vaše zařízení, zdá se, nepodporuje tento výstupní formát, ale zkusíme to", "mt_xowa": "v iOS jsou chyby bránící přehrávání na pozadí s tímto formátem; použijte prosím caf nebo mp3", "mt_tint": "úroveň pozadí (0-100) na posuvníku$Nabyste učinili ukládání do vyrovnávací paměti méně rušivým", "mt_eq": "`povoluje ekvalizér a ovládání zisku;$N$Nboost `0` = standardní 100% hlasitost (nezměněno)$N$Nwidth `1  ` = standardní stereo (nezměněno)$Nwidth `0.5` = 50% levý-pravý crossfeed$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = odstranění vokálů :^)$N$Npovolení ekvalizéru činí alba bez mezer zcela bez mezer, takže to nechte zapnuté se všemi hodnotami na nule (kromě width = 1), pokud vám na tom záleží", "mt_drc": "povoluje kompresor dynamického rozsahu (vyrovnávač hlasitosti / brickwaller); také povolí EQ pro vyvážení špaget, takže nastavte všechna EQ pole kromě 'width' na 0, pokud to nechcete$N$Nsnižuje hlasitost zvuku nad THRESHOLD dB; pro každý RATIO dB za THRESHOLD je 1 dB výstupu, takže výchozí hodnoty tresh -24 a ratio 12 znamenají, že by nikdy nemělo být hlasitější než -22 dB a je bezpečné zvýšit boost ekvalizéru na 0.8, nebo dokonce 1.8 s ATK 0 a obrovským RLS jako 90 (funguje pouze ve firefoxu; RLS je max 1 v jiných prohlížečích)$N$N(viz wikipedia, vysvětlují to mnohem lépe)", "mt_ss": "`povolí přeskočení ticha; násobí rychlost přehrávání `rych` u začátku/konce skladeb, když je hlasitost pod `hl` a pozice je v prvních `zač`% nebo posledních `kon`%", //m "mt_ssvt": "prahová hlasitost (0-255)\">hl", //m "mt_ssts": "aktivní práh (% stopy, začátek)\">zač", //m "mt_sste": "aktivní práh (% stopy, konec)\">kon", //m "mt_sssm": "násobič rychlosti přehrávání\">rych", //m "mb_play": "přehrát", "mm_hashplay": "přehrát tento audio soubor?", "mm_m3u": "stiskněte Enter/OK pro Přehrání\nstiskněte ESC/Zrušit pro Úpravu", "mp_breq": "potřebujete firefox 82+ nebo chrome 73+ nebo iOS 15+", "mm_bload": "nyní se načítá...", "mm_bconv": "převádí se na {0}, čekejte prosím...", "mm_opusen": "váš prohlížeč nemůže přehrát aac / m4a soubory;\ntranscoding na opus je nyní povolen", "mm_playerr": "přehrávání selhalo: ", "mm_eabrt": "Pokus o přehrávání byl zrušen", "mm_enet": "Vaše internetové připojení je nestabilní", "mm_edec": "Tento soubor je údajně poškozený??", "mm_esupp": "Váš prohlížeč nerozumí tomuto audio formátu", "mm_eunk": "Neznámá chyba", "mm_e404": "Nelze přehrát audio; chyba 404: Soubor nenalezen.", "mm_e403": "Nelze přehrát audio; chyba 403: Přístup odepřen.\n\nZkuste stisknout F5 pro obnovení, možná jste se odhlásili", "mm_e415": "Nelze přehrát audio; chyba 415: Převod souboru selhal; zkontrolujte logy serveru.", //m "mm_e500": "Nelze přehrát audio; chyba 500: Zkontrolujte logy serveru.", "mm_e5xx": "Nelze přehrát audio; chyba serveru ", "mm_nof": "žádné další audio soubory v okolí nenalezeny", "mm_prescan": "Hledám hudbu k dalšímu přehrání...", "mm_scank": "Další píseň nalezena:", "mm_uncache": "cache vymazána; všechny písně se znovu stáhnou při dalším přehrávání", "mm_hnf": "tato píseň již neexistuje", "im_hnf": "tento obrázek již neexistuje", "f_empty": 'tato složka je prázdná', "f_chide": 'toto skryje sloupec «{0}»\n\nmůžete odkrýt sloupce v záložce nastavení', "f_bigtxt": "tento soubor má {0} MiB -- opravdu zobrazit jako text?", "f_bigtxt2": "zobrazit pouze konec souboru? to také povolí sledování/tailing, zobrazí nově přidané řádky textu v reálném čase", "fbd_more": '
          zobrazuji {0} z {1} souborů; zobraz {2} nebo zobraz všechny
          ', "fbd_all": '
          zobrazuji {0} z {1} souborů; zobraz všechny
          ', "f_anota": "pouze {0} z {1} položek bylo vybráno;\npro výběr celé složky nejprve přejděte na konec", "f_dls": 'odkazy na soubory v aktuální složce byly\nzměněny na odkazy ke stažení', "f_dl_nd": 'přeskakuje se složka (místo toho použijte stažení zip/tar):\n', //m "f_partial": "Pro bezpečné stažení souboru, který se aktuálně nahrává, klikněte prosím na soubor se stejným názvem, ale bez přípony .PARTIAL. Stiskněte prosím Zrušit nebo Escape.\n\nStisknutím OK / Enter ignorujete toto varování a pokračujete ve stahování .PARTIAL dočasného souboru, což téměř jistě vyústí jako poškozená data.", "ft_paste": "vložit {0} položek$NKlávesová zkratka: ctrl-V", "fr_eperm": 'nelze přejmenovat:\nnemáte oprávnění “přesunout” v této složce', "fd_eperm": 'nelze smazat:\nnemáte oprávnění “smazat” v této složce', "fc_eperm": 'nelze vyjmout:\nnemáte oprávnění “přesunout” v této složce', "fp_eperm": 'nelze vložit:\nnemáte oprávnění “zapisovat” v této složce', "fr_emore": "vyberte alespoň jednu položku k přejmenování", "fd_emore": "vyberte alespoň jednu položku ke smazání", "fc_emore": "vyberte alespoň jednu položku k vyjmutí", "fcp_emore": "vyberte alespoň jednu položku k zkopírování do schránky", "fs_sc": "sdílet složku, ve které se nacházíte", "fs_ss": "sdílet vybrané soubory", "fs_just1d": "nelze vybrat více než jednu složku,\nnebo míchat soubory a složky v jednom výběru", "fs_abrt": "❌ zrušit", "fs_rand": "🎲 náhodný.název", "fs_go": "✅ vytvořit sdílení", "fs_name": "název", "fs_src": "zdroj", "fs_pwd": "heslo", "fs_exp": "vypršení", "fs_tmin": "min", "fs_thrs": "hodin", "fs_tdays": "dní", "fs_never": "navždy", "fs_pname": "volitelný název odkazu; bude náhodný, pokud je prázdný", "fs_tsrc": "soubor nebo složka ke sdílení", "fs_ppwd": "volitelné heslo", "fs_w8": "vytváření sdílení...", "fs_ok": "stiskněte Enter/OK pro zkopírování do schránky\nstiskněte ESC/Zrušit pro zavření", "frt_dec": "může opravit některé případy porušených názvů souborů\">url-decode", "frt_rst": "resetovat změněné názvy souborů zpět na původní\">↺ reset", "frt_abrt": "zrušit a zavřít toto okno\">❌ cancel", "frb_apply": "PŘEJMENOVAT", "fr_adv": "dávkové / metadata / přejmenování podle vzoru\">pokročilé", "fr_case": "regex citlivý na velikost písmen\">velikost", "fr_win": "názvy bezpečné pro windows; nahradit <>:"\\|?* japonskými plnošířkovými znaky\">win", "fr_slash": "nahradit / znakem který nezpůsobí vytvoření nových složek\">žádné /", "fr_re": "`vzor regex hledání k aplikaci na původní názvy souborů; zachycené skupiny mohou být odkazovány v poli formátu níže jako `(1)` a `(2)` atd.", "fr_fmt": "`inspirováno foobar2000:$N`(title)` je nahrazeno názvem skladby,$N`[(artist) - ](title)` přeskočí [tuto] část pokud je umělec prázdný$N`$lpad((tn),2,0)` doplní číslo stopy na 2 číslice", "fr_pdel": "smazat", "fr_pnew": "uložit jako", "fr_pname": "zadejte název pro vaše nové přednastavení", "fr_aborted": "zrušeno", "fr_lold": "starý název", "fr_lnew": "nový název", "fr_tags": "tagy pro vybrané soubory (pouze pro čtení, jen pro referenci):", "fr_busy": "přejmenovávám {0} položek...\n\n{1}", "fr_efail": "přejmenování selhalo:\n", "fr_nchg": "{0} z nových názvů bylo změněno kvůli win a/nebo žádné /\n\nPokračovat s těmito změněnými novými názvy?", "fd_ok": "mazání OK", "fd_err": "mazání selhalo:\n", "fd_none": "nic nebylo smazáno; možná blokováno konfigurací serveru (xbd)?", "fd_busy": "mažu {0} položek...\n\n{1}", "fd_warn1": "SMAZAT těchto {0} položek?", "fd_warn2": "Poslední šance! Nelze vrátit zpět. Smazat?", "fc_ok": "vyjmout {0} položek", "fc_warn": 'vyjmout {0} položek\n\nale: pouze tato karta prohlížeče je může vložit\n(protože výběr je tak absolutně masivní)', "fcc_ok": "zkopírováno {0} položek do schránky", "fcc_warn": 'zkopírováno {0} položek do schránky\n\nale: pouze tato karta prohlížeče je může vložit\n(protože výběr je tak absolutně masivní)', "fp_apply": "použít tyto názvy", "fp_skip": "přeskočit konflikty", //m "fp_ecut": "nejprve vyjměte nebo zkopírujte nějaké soubory / složky k vložení / přesunutí\n\npoznámka: můžete vyjmout / vložit přes různé karty prohlížeče", "fp_ename": "{0} položek sem nelze přesunout protože názvy jsou již obsazené. Dejte jim nové názvy níže pro pokračování, nebo název nechte prázdný (\"přeskočit konflikty\") pro přeskočení:", //m "fcp_ename": "{0} položek sem nelze zkopírovat protože názvy jsou již obsazené. Dejte jim nové názvy níže pro pokračování, nebo název nechte prázdný (\"přeskočit konflikty\") pro přeskočení:", //m "fp_emore": "stále jsou některé kolize názvů souborů k opravě", "fp_ok": "přesun OK", "fcp_ok": "kopírování OK", "fp_busy": "přesouvám {0} položek...\n\n{1}", "fcp_busy": "kopíruji {0} položek...\n\n{1}", "fp_abrt": "přerušuji...", //m "fp_err": "přesun selhal:\n", "fcp_err": "kopírování selhalo:\n", "fp_confirm": "přesunout těchto {0} položek sem?", "fcp_confirm": "zkopírovat těchto {0} položek sem?", "fp_etab": 'selhalo čtení schránky z jiné karty prohlížeče', "fp_name": "nahrávání souboru z vašeho zařízení. Dejte mu název:", "fp_both_m": '
          vyberte co vložit
          Enter = Přesunout {0} souborů z «{1}»\nESC = Nahrát {2} souborů z vašeho zařízení', "fcp_both_m": '
          vyberte co vložit
          Enter = Kopírovat {0} souborů z «{1}»\nESC = Nahrát {2} souborů z vašeho zařízení', "fp_both_b": 'PřesunoutNahrát', "fcp_both_b": 'KopírovatNahrát', "mk_noname": "napište název do textového pole vlevo předtím než to uděláte :p", "nmd_i1": "můžeš také přidat příponu souboru, například .md", //m "nmd_i2": "můžeš vytvářet pouze .{0} soubory, protože nemáš oprávnění mazat", //m "tv_load": "Načítání textového dokumentu:\n\n{0}\n\n{1}% ({2} z {3} MiB načteno)", "tv_xe1": "nelze načíst textový soubor:\n\nchyba ", "tv_xe2": "404, soubor nenalezen", "tv_lst": "seznam textových souborů v", "tvt_close": "návrat do zobrazení složky$NKlávesová zkratka: M (nebo Esc)\">❌ zavřít", "tvt_dl": "stáhnout tento soubor$NKlávesová zkratka: Y\">💾 stáhnout", "tvt_prev": "zobrazit předchozí dokument$NKlávesová zkratka: i\">⬆ předchozí", "tvt_next": "zobrazit následující dokument$NKlávesová zkratka: K\">⬇ další", "tvt_sel": "vybrat soubor   ( pro vyjmutí / kopírování / mazání / ... )$NKlávesová zkratka: S\">výběr", "tvt_j": "zkrášlit json$NKlávesová zkratka: shift-J\">j", //m "tvt_edit": "otevřít soubor v textovém editoru$NKlávesová zkratka: E\">✏️ upravit", "tvt_tail": "sledovat soubor pro změny; zobrazit nové řádky v reálném čase\">📡 sledovat", "tvt_wrap": "zalamování slov\">↵", "tvt_atail": "zamknout posun na konec stránky\">⚓", "tvt_ctail": "dekódovat barvy terminálu (ansi escape kódy)\">🌈", "tvt_ntail": "limit zpětného posouvání (kolik bajtů textu ponechat načtených)", "m3u_add1": "skladba přidána do m3u playlistu", "m3u_addn": "{0} skladeb přidáno do m3u playlistu", "m3u_clip": "m3u playlist nyní zkopírován do schránky\n\nměli byste vytvořit nový textový soubor pojmenovaný něco.m3u a vložit playlist do tohoto dokumentu; toto ho učiní přehratelným", "gt_vau": "nezobrazovat videa, jen přehrát zvuk\">🎧", "gt_msel": "povolit výběr souborů; ctrl-klik na soubor pro přepsání$N$N<em>když aktivní: dvojklik na soubor / složku pro otevření</em>$N$NKlávesová zkratka: S\">výběr více", "gt_crop": "ořez náhledů na střed\">ořez", "gt_3x": "náhledy s vysokým rozlišením\">3x", "gt_zoom": "zoom", "gt_chop": "rozdělit", "gt_sort": "řadit podle", "gt_name": "název", "gt_sz": "velikost", "gt_ts": "datum", "gt_ext": "typ", "gt_c1": "více zkrátit názvy souborů (zobrazit méně)", "gt_c2": "méně zkrátit názvy souborů (zobrazit více)", "sm_w8": "hledám...", "sm_prev": "výsledky hledání níže jsou z předchozího dotazu:\n ", "sl_close": "zavřít výsledky hledání", "sl_hits": "zobrazuji {0} zásahů", "sl_moar": "načíst více", "s_sz": "velikost", "s_dt": "datum", "s_rd": "cesta", "s_fn": "název", "s_ta": "tagy", "s_ua": "nahráno@", "s_ad": "pokročilé", "s_s1": "minimum MiB", "s_s2": "maximum MiB", "s_d1": "min. iso8601", "s_d2": "max. iso8601", "s_u1": "nahráno po", "s_u2": "a/nebo před", "s_r1": "cesta obsahuje   (oddělené mezerami)", "s_f1": "název obsahuje   (negace s -ne)", "s_t1": "tagy obsahují   (^=začátek, konec=$)", "s_a1": "specifické vlastnosti metadat", "md_eshow": "nelze vykreslit ", "md_off": "[📜readme] zakázáno v [⚙️] -- dokument skryt", "badreply": "Selhalo parsování odpovědi ze serveru", "xhr403": "403: Přístup odepřen\n\nzkuste stisknout F5, možná jste se odhlásili", "xhr0": "neznámý (pravděpodobně ztraceno spojení se serverem, nebo server je offline)", "cf_ok": "omlouváme se za to -- DD" + wah + "oS ochrana se aktivovala\n\nvěci by se měly obnovit asi za 30 sekund\n\npokud se nic nestane, stiskněte F5 pro obnovení stránky", "tl_xe1": "nelze vypsat podsložky:\n\nchyba ", "tl_xe2": "404: Složka nenalezena", "fl_xe1": "nelze vypsat soubory ve složce:\n\nchyba ", "fl_xe2": "404: Složka nenalezena", "fd_xe1": "nelze vytvořit podsložku:\n\nchyba ", "fd_xe2": "404: Nadřazená složka nenalezena", "fsm_xe1": "nelze odeslat zprávu:\n\nchyba ", "fsm_xe2": "404: Nadřazená složka nenalezena", "fu_xe1": "selhalo načtení unpost seznamu ze serveru:\n\nchyba ", "fu_xe2": "404: Soubor nenalezen??", "fz_tar": "nekomprimovaný gnu-tar soubor (linux / mac)", "fz_pax": "nekomprimovaný tar formátu pax (pomalejší)", "fz_targz": "gnu-tar s gzip kompresí úrovně 3$N$Nto je obvykle velmi pomalé, takže$Npoužijte místo toho nekomprimovaný tar", "fz_tarxz": "gnu-tar s xz kompresí úrovně 1$N$Nto je obvykle velmi pomalé, takže$Npoužijte místo toho nekomprimovaný tar", "fz_zip8": "zip s utf8 názvy souborů (možná problematické na windows 7 a starších)", "fz_zipd": "zip s tradičními cp437 názvy souborů, pro opravdu starý software", "fz_zipc": "cp437 s crc32 vypočítaným brzy,$Npro MS-DOS PKZIP v2.04g (říjen 1993)$N(trvá déle zpracovat před začátkem stahování)", "un_m1": "můžete smazat vaše nedávné nahrání (nebo zrušit nedokončené) níže", "un_upd": "obnovit", "un_m4": "nebo sdílet soubory viditelné níže:", "un_ulist": "zobrazit", "un_ucopy": "kopírovat", "un_flt": "volitelný filtr:  URL musí obsahovat", "un_fclr": "vymazat filtr", "un_derr": 'unpost-delete selhalo:\n', "un_f5": 'něco se pokazilo, zkuste prosím obnovit, nebo stiskněte F5', "un_uf5": "omlouváme se ale musíte obnovit stránku (například stisknutím F5 nebo CTRL-R) předtím než toto nahrávání může být zrušeno", "un_nou": 'varování: server je příliš zaneprázdněn pro zobrazení nedokončených nahrávání; za chvíli klikněte na odkaz "obnovit"', "un_noc": 'varování: unpost plně nahraných souborů není povoleno/dovoleno v konfiguraci serveru', "un_max": "zobrazuji prvních 2000 souborů (použijte filtr)", "un_avail": "{0} nedávných nahrávání může být smazáno
          {1} nedokončených může být zrušeno", "un_m2": "řazeno podle času nahrávání; nejnovější první:", "un_no1": "počkej! žádná nahrávání nejsou dostatečně nedávná", "un_no2": "počkej! žádná nahrávání odpovídající tomuto filtru nejsou dostatečně nedávná", "un_next": "smazat dalších {0} souborů níže", "un_abrt": "zrušit", "un_del": "smazat", "un_m3": "načítám vaše nedávné nahrání...", "un_busy": "mažu {0} souborů...", "un_clip": "{0} odkazů zkopírováno do schránky", "u_https1": "měli byste", "u_https2": "přejít na https", "u_https3": "pro lepší výkon", "u_ancient": "váš prohlížeč je úctyhodně starý -- možná byste měli použít bup", "u_nowork": "vyžadován firefox 53+ nebo chrome 57+ nebo iOS 11+", "tail_2old": "vyžadován firefox 105+ nebo chrome 71+ nebo iOS 14.5+", "u_nodrop": "váš prohlížeč je příliš starý pro nahrávání přetažením (drag-and-drop)", "u_notdir": "toto není složka!\n\nváš prohlížeč je příliš starý,\nzkuste prosím soubory přetáhnout", "u_uri": "pro přetažení obrázků z jiných oken prohlížeče,\nje prosím přetáhněte na velké tlačítko pro nahrávání", "u_enpot": "přepnout na potato UI (může zrychlit nahrávání)", "u_depot": "přepnout na fancy UI (může zpomalit nahrávání)", "u_gotpot": "přepínám na potato UI pro zrychlení nahrávání,\n\npokud nesouhlasíte, klidně jej přepněte zpět!", "u_pott": "

          soubory:   {0} dokončeno,   {1} selhalo,   {2} nahrává se,   {3} ve frontě

          ", "u_ever": "toto je základní nahrávání; up2k vyžaduje alespoň
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": "toto je základní nahrávání; up2k je lepší", "u_uput": "optimalizovat pro rychlost (přeskočit kontrolní součet)", "u_ewrite": "nemáte oprávnění k zápisu do této složky", "u_eread": "nemáte oprávnění ke čtení této složky", "u_enoi": "vyhledávání souborů není povoleno v konfiguraci serveru", "u_enoow": "přepsání zde nebude fungovat; je vyžadováno oprávnění k mazání", "u_badf": "Těchto {0} souborů (z celkem {1}) bylo přeskočeno, pravděpodobně kvůli oprávněním v souborovém systému:\n\n", "u_blankf": "Těchto {0} souborů (z celkem {1}) je prázdných; přesto je nahrát?\n\n", "u_applef": "Těchto {0} souborů (z celkem {1}) je pravděpodobně nežádoucích;\nStiskněte OK/Enter pro PŘESKOČENÍ následujících souborů,\nStiskněte Zrušit/ESC pro Zahrnutí a NAHRÁNÍ i těchto souborů:\n\n", "u_just1": "\nMožná to bude fungovat lépe, když vyberete pouze jeden soubor", "u_ff_many": "pokud používáte Linux / MacOS / Android, takové množství souborů může shodit Firefox!\npokud se to stane, zkuste to prosím znovu (nebo použijte Chrome).", "u_up_life": "Tento upload bude smazán ze serveru\n{0} po jeho dokončení", "u_asku": "Nahrát {0} souborů do {1}", "u_unpt": "toto nahrávání můžete vrátit zpět / smazat pomocí 🧯 vlevo nahoře", "u_bigtab": "chystám se zobrazit {0} souborů\n\nto může shodit váš prohlížeč, jste si jisti?", "u_scan": "Skenuji soubory...", "u_dirstuck": "procházení adresáře se zaseklo při pokusu o přístup k následujícím {0} položkám; budou přeskočeny:", "u_etadone": "Hotovo ({0}, {1} souborů)", "u_etaprep": "(příprava na nahrávání)", "u_hashdone": "hashování dokončeno", "u_hashing": "hashování", "u_hs": "navazuji spojení...", "u_started": "soubory se nyní nahrávají; viz [🚀]", "u_dupdefer": "duplikát; bude zpracován po všech ostatních souborech", "u_actx": "klikněte na tento text, abyste zabránili ztrátě
          výkonu při přepínání do jiných oken/záložek", "u_fixed": "OK!  Opraveno 👍", "u_cuerr": "nepodařilo se nahrát část {0} z {1};\npatrně neškodné, pokračuji\n\nsoubor: {2}", "u_cuerr2": "server odmítl nahrání (část {0} z {1});\nzopakuji později\n\nsoubor: {2}\n\nchyba ", "u_ehstmp": "zopakuji pokus; viz vpravo dole", "u_ehsfin": "server odmítl požadavek na dokončení nahrávání; opakuji pokus...", "u_ehssrch": "server odmítl požadavek na vyhledávání; opakuji pokus...", "u_ehsinit": "server odmítl požadavek na zahájení nahrávání; opakuji pokus...", "u_eneths": "síťová chyba při navazování spojení pro nahrávání; opakuji pokus...", "u_enethd": "síťová chyba při ověřování existence cíle; opakuji pokus...", "u_cbusy": "čekám, až nám server po síťovém problému začne znovu důvěřovat...", "u_ehsdf": "na serveru došlo místo na disku!\n\nbudu to zkoušet dál, pro případ, že někdo\nuvolní dostatek místa pro pokračování", "u_emtleak1": "vypadá to, že váš webový prohlížeč může mít únik paměti (memory leak);\nprosím", "u_emtleak2": " přejděte na https (doporučeno) nebo ", "u_emtleak3": " ", "u_emtleakc": "zkuste následující:\n
          • stiskněte F5 pro obnovení stránky
          • poté vypněte tlačítko  mt  v  ⚙️ nastavení
          • a zkuste nahrávání znovu
          Nahrávání bude o něco pomalejší, ale co se dá dělat.\nOmlouváme se za potíže!\n\nPS: chrome v107 obsahuje opravu pro tento problém", "u_emtleakf": "zkuste následující:\n
          • stiskněte F5 pro obnovení stránky
          • poté zapněte 🥔 (potato) v rozhraní nahrávání
          • a zkuste nahrávání znovu
          \nPS: firefox snad bude mít opravu v některé z příštích verzí", "u_s404": "nenalezeno na serveru", "u_expl": "vysvětlit", "u_maxconn": "většina prohlížečů omezuje počet na 6, ale firefox umožňuje toto navýšit pomocí connections-per-server v about:config", "u_tu": "

          VAROVÁNÍ: turbo zapnuto,  klient nemusí detekovat a obnovit nedokončené nahrávání; viz nápovědu u tlačítka turbo

          ", "u_ts": "

          VAROVÁNÍ: turbo zapnuto,  výsledky vyhledávání mohou být nesprávné; viz nápovědu u tlačítka turbo

          ", "u_turbo_c": "turbo je vypnuto v konfiguraci serveru", "u_turbo_g": "vypínám turbo, protože nemáte oprávnění\nk výpisu adresářů na tomto svazku", "u_life_cfg": 'automatické smazání po min (nebo hodinách)', "u_life_est": 'nahrání bude smazáno ---', "u_life_max": 'tato složka vynucuje\nmax. životnost {0}', "u_unp_ok": 'unpost je povoleno pro {0}', "u_unp_ng": 'unpost NEBUDE povoleno', "ue_ro": 'váš přístup k této složce je pouze pro čtení\n\n', "ue_nl": 'momentálně nejste přihlášeni', "ue_la": 'momentálně jste přihlášeni jako "{0}"', "ue_sr": 'momentálně jste v režimu vyhledávání souborů\n\npřepněte do režimu nahrávání kliknutím na lupu 🔎 (vedle velkého tlačítka HLEDAT) a zkuste nahrávání znovu\n\nomlouváme se', "ue_ta": 'zkuste nahrávání znovu, nyní by to mělo fungovat', "ue_ab": "tento soubor se již nahrává do jiné složky a toto nahrávání musí být dokončeno předtím, než může být soubor nahrán jinam.\n\nMůžete zrušit a zapomenout na původní nahrávání pomocí levého horního 🧯", "ur_1uo": "OK: Soubor úspěšně nahrán", "ur_auo": "OK: Všech {0} souborů úspěšně nahráno", "ur_1so": "OK: Soubor nalezen na serveru", "ur_aso": "OK: Všech {0} souborů nalezeno na serveru", "ur_1un": "Nahrání selhalo, omlouváme se", "ur_aun": "Všech {0} nahrání selhalo, omlouváme se", "ur_1sn": "Soubor NEBYL nalezen na serveru", "ur_asn": "{0} souborů NEBYLO nalezeno na serveru", "ur_um": "Dokončeno;\n{0} nahrání OK,\n{1} nahrání selhalo, omlouváme se", "ur_sm": "Dokončeno;\n{0} souborů nalezeno na serveru,\n{1} souborů NENALEZENO na serveru", "rc_opn": "otevřít", //m "rc_ply": "přehrát", //m "rc_pla": "přehrát jako zvuk", //m "rc_txt": "otevřít v prohlížeči souborů", //m "rc_md": "otevřít v textovém editoru", //m "rc_dl": "stáhnout", //m "rc_zip": "stáhnout jako archiv", //m "rc_cpl": "kopírovat odkaz", //m "rc_del": "smazat", //m "rc_cut": "vyjmout", //m "rc_cpy": "kopírovat", //m "rc_pst": "vložit", //m "rc_rnm": "přejmenovat", //m "rc_nfo": "nová složka", //m "rc_nfi": "nový soubor", //m "rc_sal": "vybrat vše", //m "rc_sin": "invertovat výběr", //m "rc_shf": "sdílet tuto složku", //m "rc_shs": "sdílet výběr", //m "lang_set": "obnovit stránku, aby se změna projevila?", "splash": { "a1": "obnovit", "b1": "ahoj cizinče   (nejsi přihlášen)", "c1": "odhlásit se", "d1": "vypsat zásobníku", "d2": "zobrazit stav všech aktivních vláken", "e1": "znovu načíst konfiguraci", "e2": "znovu načíst konfigurační soubory (accounts/volumes/volflags),$Na prohledat všechny e2ds úložiště$N$Npoznámka: všechny změny globálních nastavení$Nvyžadují úplné restartování, aby se projevily", "f1": "můžeš procházet:", "g1": "můžeš nahrávat do:", "cc1": "další věci:", "h1": "zakázat k304", "i1": "povolit k304", "j1": "povolení k304 odpojí vašeho klienta při každém HTTP 304, což může zabránit některým chybovým proxy serverům, aby se zasekly (náhle nenačítaly stránky), ale také to obecně zpomalí věci", "k1": "resetovat nastavení klienta", "l1": "přihlaste se pro více:", "ls3": "přihlásit se", //m "lu4": "uživatelské jméno", //m "lp4": "heslo", //m "lo3": "odhlásit “{0}” všude", //m "lo2": "tímto ukončíte relaci ve všech prohlížečích", //m "m1": "vítej zpět,", "n1": "404 nenalezeno  ┐( ´ -`)┌", "o1": 'nebo možná nemáš přístup -- zkus heslo nebo jdi domů', "p1": "403 zakázáno  ~┻━┻", "q1": 'použij heslo nebo jdi domů', "r1": "jdi domů", ".s1": "znovu prohledat", "t1": "akce", "u2": "čas od posledního zápisu na server$N( upload / rename / ... )$N$N17d = 17 dní$N1h23 = 1 hodina 23 minut$N4m56 = 4 minuty 56 sekund", "v1": "připojit", "v2": "použít tento server jako místní HDD", "w1": "přepnout na https", "x1": "změnit heslo", "y1": "upravit sdílení", "z1": "odblokovat toto sdílení:", "ta1": "nejprve vyplňte své nové heslo", "ta2": "zopakujte pro potvrzení nového hesla:", "ta3": "nalezen překlep; zkuste to prosím znovu", "nop": "CHYBA: Heslo nesmí být prázdné", //m "nou": "CHYBA: Uživatelské jméno a/nebo heslo nesmí být prázdné", //m "aa1": "příchozí soubory:", "ab1": "deaktivovat no304", "ac1": "povolit no304", "ad1": "povolení no304 deaktivuje veškeré mezipaměti; zkuste to, pokud k304 nestačilo. To ovšem zapříčíní obrovské množství síťového provozu!", "ae1": "aktivní stahování:", "af1": "zobrazit nedávné nahrávání", "ag1": "zobrazit známé uživatele IdP", //m } }; ================================================ FILE: copyparty/web/tl/deu.js ================================================ // Zeilen, die mit //m enden, sind nicht verifizierte maschinelle Übersetzungen Ls.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", "Song 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)"], ["Y", "Textdatei herunterladen"], ["⇧ J", "json verschönern"], ] ], "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 ", "login": "Anmelden", "access": " Zugriff", "ot_close": "Submenu schliessen", "ot_search": "`Dateien nach Attributen, Pfad/Name, Musiktags oder beliebiger Kombination suchen$N$N`foo bar` = muss «foo» und «bar» enthalten,$N`foo -bar` = muss «foo» aber nicht «bar» enthalten,$N`^yana .opus$` = beginnt mit «yana» und ist «opus»-Datei$N`"try unite"` = genau «try unite» enthalten$N$NDatumsformat ist iso-8601, z.B.$N`2009-12-31` oder `2020-09-12 23:30:00`", "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-file: neue Textdatei 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": "Textdatei 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$N⏭️: Vorhandene Dateien immer überspringen", "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_hfsz": "Dateigröße", "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", "cl_rcm": "Rechtsklick-Menü", "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_dsel": 'Ziehauswahl in Raster-Ansicht verwenden">ziehen', //m "ct_dl": 'Beim Klick auf Dateien sie immer herunterladen (nicht einbetten)">dl', "ct_ihop": 'Wenn die Bildanzeige geschlossen ist, scrolle runter zu den zuletzt angesehenen Dateien">g⮯', "ct_dots": 'Verstecke Dateien anzeigen (wenn durch den Server erlaubt)">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_utc": 'Für alle Zeitangaben UTC verwenden">UTC', "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 (`,sorthref`) in Media-URLs enthalten sein sollen. Ein Wert von 0 sorgt dafür, dass Sortierregeln in Media-URLs ignoriert werden", "cdt_ren": "spezielles Rechtsklick-Menü aktivieren, das Browser-Menü ist weiterhin mit Shift + Rechtsklick erreichbar\">aktivieren", "cdt_rdb": "normales Rechtsklick-Menü anzeigen, wenn das benutzerdefinierte bereits offen ist und erneut rechts geklickt wird\">x2", //m "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", "ml_ss": "Stille Überspringen", //m "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 {0} konvertierebn\">flac", "mt_caac": "AAC / M4A zu {0} konvertieren\">aac", "mt_coth": "Convertiere alle Dateien (die nicht MP3 sind) zu {0}\">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_c2flac": "beste Klangqualität, aber riesige Downloads\">flac", "mt_c2wav": "unkomprimierte Wiedergabe (noch größer)\">wav", "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 `0` = Standard 100% Lautstärke (unverändert)$N$Nwidth `1  ` = Standard Stereo (unverändert)$Nwidth `0.5` = 50% Links-Rechts-Crossfeed$Nwidth `0  ` = Mono$N$Nboost `-0.8` & width `10` = 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)", "mt_ss": "`Aktiviert Stille-Überspringen; multipliziert die Wiedergabegeschwindigkeit mit `vor`, nahe Anfang/Ende von Titeln, wenn die Lautstärke unter `laut` liegt und die Position innerhalb der ersten `anf`% oder letzten `end`% ist", //m "mt_ssvt": "Lautstärkeschwelle (0-255)\">laut", //m "mt_ssts": "Aktiv-Schwelle (% Titel, Anfang)\">anf", //m "mt_sste": "Aktiv-Schwelle (% Titel, Ende)\">end", //m "mt_sssm": "Wiedergabegeschwindigkeits-Multiplikator\">vor", //m "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_e415": "Konnte Datei nicht abspielen; Fehler 415: Umwandlung der Datei fehlgeschlagen; Serverlogs prüfen.", //m "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_dl_nd": 'ordner wird übersprungen (bitte zip/tar-download verwenden):\n', //m "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 `(1)` und `(2)` usw. referenziert werden", "fr_fmt": "`inspiriert von foobar2000:$N`(title)` wird durch Songtitel ersetzt,$N`[(artist) - ](title)` überspringt [diesen] Teil falls Interpret leer$N`$lpad((tn),2,0)` 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_skip": "Konflikte überspringen", "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 um sie zu überspringen ("Konflikte überspringen" macht das automatisch):', "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 um sie zu überspringen ("Konflikte überspringen" macht das automatisch):', "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_abrt": "wird abgebrochen...", "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", "nmd_i1": "Füge auch die Dateiendung hinzu, z.B. .md", "nmd_i2": "Du kannst nur .{0}-Dateien erstellen, da dir Lösch-Rechte fehlen", "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_j": "json verschönern$NHotkey: shift-J\">j", "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 (langsamer)", "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 früh berechnetem CRC32,$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", "rc_opn": "öffnen", "rc_ply": "abspielen", "rc_pla": "als Audio abspielen", "rc_txt": "als Text öffnen", "rc_md": "im Texteditor öffnen", "rc_dl": "herunterladen", "rc_zip": "als Archiv herunterladen", "rc_cpl": "Link kopieren", //m "rc_del": "löschen", "rc_cut": "ausschneiden", "rc_cpy": "kopieren", "rc_pst": "einfügen", "rc_rnm": "umbenennen", //m "rc_nfo": "neuer Ordner", "rc_nfi": "neue Datei", "rc_sal": "alles auswählen", "rc_sin": "auswahl umkehren", "rc_shf": "diesen ordner teilen", //m "rc_shs": "auswahl teilen", //m "lang_set": "Neuladen um Änderungen anzuwenden?", "splash": { "a1": "Neu laden", "b1": "Tach, wie geht's?   (Du bist nicht angemeldet)", "c1": "Abmelden", "d1": "Zustand", "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", "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:", "ls3": "Anmelden", "lu4": "Benutzername", "lp4": "Passwort", "lo3": "“{0}” überall abmelden", "lo2": "Das beendet die Sitzung in allen Browsern", "m1": "Willkommen zurück,", "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", "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", "z1": "Share entsperren:", "ta1": "Trage zuerst dein Passwort ein", "ta2": "Wiederhole dein Passwort zur Bestätigung:", "ta3": "Da stimmt etwas nicht; probier's nochmal", "nop": "FEHLER: Passwort darf nicht leer sein", "nou": "FEHLER: Benutzername und/oder Passwort dürfen nicht leer sein", "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", "ag1": "Bekannte IdP-Benutzer anzeigen", //m } }; ================================================ FILE: copyparty/web/tl/epo.js ================================================ // Linioj, finiĝantaj per "//m", estas nekontrolitaj maŝinaj tradukoj Ls.epo = { "tt": "Esperanto", "cols": { "c": "ago-butonoj", "dur": "daŭro", "q": "kvalito / bitrapido", "Ac": "sonkodeko", "Vc": "videokodeko", "Fmt": "formato / ujo", "Ahash": "kontrolsumo de aŭdio", "Vhash": "kontrolsumo de video", "Res": "distingivo", "T": "dosiertipo", "aq": "kvalito / bitrapido de aŭdio", "vq": "kvalito / bitrapido de video", "pixfmt": "specimenado / strukturo de bilderoj", "resw": "horizontala distingivo", "resh": "vertikala distingivo", "chs": "nombro de aŭdio-kanaloj", "hz": "sonpecrapido", }, "hks": [ [ "misc", ["ESK", "malfermi variajn aferojn"], "file-manager", ["G", "baskuli inter lista kaj krada vido"], ["T", "baskuli montradon de bildetoj"], ["⇧ A/D", "grandeco de bildetoj"], ["stir-K", "forigi elektitajn"], ["stir-X", "eltondi elektaĵon al tondujo"], ["stir-C", "kopii elektaĵon al tondujo"], ["stir-V", "alglui (movi/kopii) ĉi tien"], ["Y", "elŝuti elektitajn"], ["F2", "alinomi elektitajn"], "file-list-sel", ["spacoklavo", "baskuli elektadon de dosieroj"], ["↑/↓", "movi elektado-kursoron"], ["stir ↑/↓", "movi kursoron kaj vidujon"], ["⇧ ↑/↓", "elekti (mal)sekvan dosieron"], ["stir-A", "elekti ĉiujn dosier(uj)ojn"], ], [ "navigation", ["B", "baskuli inter paĝnivela kaj arbovida navigo"], ["I/K", "(mal-)sekva dosierujo"], ["M", "parent folder (or unexpand current)"], ["V", "baskuli inter montrado de dosierujoj aŭ tekst-dosieroj en toggle folders / textfiles en arbovida navig-panelo"], ["A/D", "grandeco de arbovida navig-panelo"], ], [ "audio-player", ["J/L", "(mal-)sekva kanto"], ["U/O", "iri 10 sekundoj (mal)antaŭen"], ["0..9", "iri al 0%..90%"], ["P", "ludi/paŭzigi (ankaŭ komencas)"], ["S", "elekti ludantan kanton"], ["Y", "elŝuti kanton"], ], [ "image-viewer", ["J/L, ←/→", "(mal)sekva bildo"], ["Home/End", "unua/lasta bildo"], ["F", "plenekrana vido"], ["R", "turni dekstrumen"], ["⇧ R", "turni maldekstrumen"], ["S", "elekti bildon"], ["Y", "elŝuti bildon"], ], [ "video-player", ["U/O", "iri 10 sekundoj (mal)antaŭen"], ["P/K/Spaco", "ludi/paŭzigi"], ["C", "??continue playing next"], ["V", "cikla ludado"], ["M", "silentigi"], ["[ and ]", "agordi intervalon de cikla ludado"], ], [ "textfile-viewer", ["I/K", "(mal)sekva dosiero"], ["M", "fermi dosieron"], ["E", "redakti dosieron"], ["S", "elekti dosieron (por eltondado/kopiado/alinomado)"], ["Y", "elŝuti tekstodosieron"], ["⇧ J", "beligi JSONon"], ] ], "m_ok": "OK", "m_ng": "Rezigni", "enable": "Ŝalti", "danger": "DANĜERO", "clipped": "kopiita al tondujo", "ht_s1": "sekundo", "ht_s2": "sekundoj", "ht_m1": "minuto", "ht_m2": "minutoj", "ht_h1": "horo", "ht_h2": "horoj", "ht_d1": "tago", "ht_d2": "tagoj", "ht_and": " kaj ", "goh": "stirpanelo", "gop": 'malsekva dosierujo">malsekva', "gou": 'supra dosierujo">supren', "gon": 'sekva dosierujo">sekva', "logout": "Adiaŭi kiel ", "login": "Ensaluti", "access": " atingo", "ot_close": "fermi submenuon", "ot_search": "`serĉi dosierojn per atributoj, indiko / nomo, etikedoj de muziko aŭ ĉiu kombinaĵo de tiuj parametroj$N$N`foo bar` = devas enhavi ambaŭ «foo» kaj «bar»,$N`foo -bar` = devas enhavi «foo», sed ne «bar»,$N`^yana .opus$` = komenci per «yana» kaj esti dosiero de formato «opus»$N`"try unite"` = enhavi precipe «try unite»$N$Nformato de datoj estas iso-8601, ekzemple$N`2009-12-31` aŭ `2020-09-12 23:30:00`", "ot_unpost": "unpost: forigi viaj plej lastaj alŝutoj, aŭ ĉesigi nefinigitajn", "ot_bup": "bup: fundamenta alŝutilo, funkias eĉ kun netscape 4.0", "ot_mkdir": "mkdir: krei novan dosierujon", "ot_md": "new-file: krei novan tekstodosieron", "ot_msg": "msg: sendi mesaĝon al servila protokolo", "ot_mp": "agordoj de medialudilo", "ot_cfg": "aliaj agordoj", "ot_u2i": 'up2k: alŝuti dosierojn (se vi havas skribo-atingon) aŭ ŝalti ŝerc-reĝimon por determini, ĉu dosieroj jam ekzistas ie ĉe la servilo$N$Nalŝutoj estas daŭrigeblaj, plurfadenaj, kaj prezervas tempindikojn, sed ĝi estas pli procesor-intensa ol [🎈]  (la fundamenta alŝutilo)

          dum alŝutado, ĉi tiu simbolo iĝas plenumindikilo!', "ot_u2w": 'up2k: alŝuti dosierojn kun subteno de daŭrigeblo (fermu vian retumilon kaj demetu la samajn dosierojn poste)$N$Nalŝutoj estas daŭrigeblaj, plurfadenaj, kaj prezervas tempindikojn, sed ĝi estas pli procesor-intensa ol [🎈]  (la fundamenta alŝutilo)

          dum alŝutado, ĉi tiu simbolo iĝas plenumindikilo!', "ot_noie": 'Bonvolu uzi retumilojn Chrome / Firefox / Edge', "ab_mkdir": "krei dosierujon", "ab_mkdoc": "krei tekstodosieron", "ab_msg": "sendi mesaĝon al protokolo", "ay_path": "iri al dosierujoj", "ay_files": "iri al dosieroj", "wt_ren": "alinomi elektitajn aĵojn$NFulmoklavo: F2", "wt_del": "forigi elektitajn aĵojn$NFulmoklavo: stir-K", "wt_cut": "eltondi elektitajn aĵojn <small>(do alglui ien aliloke)</small>$NFulmoklavo: stir-X", "wt_cpy": "kopii elektitajn aĵojn al tondujo$N(por alglui ien aliloke)$NFulmoklavo: stir-C", "wt_pst": "alglui antaŭe eltonditajn / kopiitajn aĵojn$NFulmoklavo: stir-V", "wt_selall": "elekti ĉiujn dosierojn$NFulmoklavo: stir-A (se dosiero estas elektita)", "wt_selinv": "inversigi elektaĵon", "wt_zip1": "elŝuti dosierujon kiel arkivo", "wt_selzip": "elŝuti elektaĵon kiel arkivo", "wt_seldl": "elŝuti elektaĵon kiel apartaj dosieroj$NFulmoklavo: Y", "wt_npirc": "kopii IRC-formatan muzikaĵ-informon", "wt_nptxt": "kopii tekstan muzikaĵ-informon", "wt_m3ua": "aldoni al m3u-ludliston (klaku butonon 📻copy poste)", "wt_m3uc": "kopii m3u-ludliston al tondujo", "wt_grid": "baskuli kradan / listan vidon$NFulmoklavo: G", "wt_prev": "malsekva muzikaĵo$NFulmoklavo: J", "wt_play": "ludi / paŭzigi$NFulmoklavo: P", "wt_next": "sekva muzikaĵo$NFulmoklavo: L", "ul_par": "paralelaj alŝutoj:", "ut_rand": "hazardigi dosiernomojn", "ut_u2ts": "kopii la tempon de lasta modifo$Nel via dosiersistemo al la servilo\">📅", "ut_ow": "ĉu anstataŭigi dosierojn ĉe la servilo?$N🛡️: neniam (dosiero estos alŝutita kun nova dosiernomo)$N🕒: anstataŭigi, se servila dosiero estas pli malnova ol via$N♻️: ĉiam anstataŭigi, se dosieroj estas malsamaj$N⏭️: senkondiĉe preterlasi ĉiujn ekzistantajn dosierojn", "ut_mt": "daŭri kalkuladon de kontrolsumoj por aliaj dosieroj dum alŝutado$N$Nmalŝaltinda, se via procesoro aŭ disko ne estas sufiĉe rapidaj", "ut_ask": 'peti konfirmon antaŭ komenco de alŝutado">💭', "ut_pot": "plirapidigi alŝutadon por malrapidaj komputiloj$Nper malkomplikado de fasado", "ut_srch": "ne alŝuti ion ajn, nur kontroli, ke la dosieroj $N jam ekzistas ĉe la servilo (ĉiuj dosierujoj, kiuj vi povas legi, estos skanitaj)", "ut_par": "paŭzi alŝutadon per agordado kiel 0$N$Npligrandigi, se via konekto estas malrapida aŭ malfruema$N$Nagordi kiel 1, se la loka reto aŭ servila disko ne estas sufiĉe rapidaj", "ul_btn": "demeti dosier(uj)ojn
          ĉi tien (aŭ alklaki ĉi tien)", "ul_btnu": "A L Ŝ U T I", "ul_btns": "S E R Ĉ I", "ul_hash": "k-sumado", "ul_send": "sendado", "ul_done": "finita", "ul_idle1": "neniuj alŝutoj envicigitaj", "ut_etah": "meza rapido de <em>kontrolsumado</em>, kaj pritaksita tempo ĝis la fino", "ut_etau": "meza rapido de <em>alŝutado</em>, kaj pritaksita tempo ĝis la fino", "ut_etat": "meza <em>tuta</em> rapido, kaj pritaksita tempo ĝis la fino", "uct_ok": "sukcese plenumita", "uct_ng": "malbona: malsukceso / malakcepto / ne trovita (no good)", "uct_done": "ambaŭ ok kaj ng", "uct_bz": "kontrolsumado aŭ alŝutado (busy)", "uct_q": "envicigita (queue)", "utl_name": "dosiernomo", "utl_ulist": "listigi", "utl_ucopy": "kopii", "utl_links": "ligilojn", "utl_stat": "stato", "utl_prog": "progreso", // keep short: "utl_404": "404", "utl_err": "ERARO", "utl_oserr": "OS-eraro", "utl_found": "trovita", "utl_defer": "postigita", "utl_yolo": "rapidega", "utl_done": "finita", "ul_flagblk": "la dosieroj estis aldonita al la vico,
          sed alia langeto de retumilo jam alŝutas dosierojn per up2k,
          do tiu alŝutado devas finiĝi unue", "ul_btnlk": "la agordado de servilo ne permesas ŝanĝi tiun agordon", "udt_up": "Alŝuti", "udt_srch": "Serĉi", "udt_drop": "demetu ĉi tien", "u_nav_m": '
          do, kion vi havas ĉi tie?
          Enter = Dosierojn (unu al multaj)\nESK = Unu dosierujon (eble kun subdosierujoj)', "u_nav_b": 'DosierojnUnu dosierujo', "cl_opts": "ŝaltiloj", "cl_hfsz": "dosiergrando", "cl_themes": "etoso", "cl_langs": "lingvo", "cl_ziptype": "elŝutado de dosieroj", "cl_uopts": "agordoj de up2k", "cl_favico": "retpaĝsimbolo", "cl_bigdir": "grandaj ujoj", "cl_hsort": "#ordigo", "cl_keytype": "skemo de fulmoklavoj", "cl_hiddenc": "kaŝitaj kolumnoj", "cl_hidec": "kaŝi", "cl_reset": "restarigi", "cl_hpick": "alklaki la kapojn de kolumnoj por kasi en la suban tabelon", "cl_hcancel": "kaŝado de kolumno nuligita", "cl_rcm": "dekstra-klaka menuo", "ct_grid": '田 krado', "ct_ttips": '◔ ◡ ◔">ℹ️ ŝpruchelpiloj', "ct_thumb": 'dum krado-vido, baskuli montradon de simboloj aŭ bildetoj$NFulmoklavo: T">🖼️ bildetoj', "ct_csel": 'uzi STIR kaj MAJ por elekti dosierojn en krado-vido">elekto', "ct_dsel": 'uzi tren-elekton en krado-vido">treni', "ct_dl": 'devigi elŝuton (ne montri enkadre), kiam dosiero estas alklakita">elŝuti', "ct_ihop": 'rulumi al la lasta vidita bildo-dosiero post fermado de bildo-vidilo">🖼️⮯', "ct_dots": 'montri kaŝitajn dosierojn (se servilo permesas)">kaŝitaj', "ct_qdel": 'peti konfirmon nur unufoje antaŭ forigado">rapid-forig.', "ct_dir1st": 'ordigi dosierujojn antaŭ dosieroj">📁 unue', "ct_nsort": '`numera ordigo de dosiernomoj (ekz. `2` antaŭ `11`)">№.ord', "ct_utc": 'montri ĉiuj datoj kaj tempoj per UTC">UTC', "ct_readme": 'montri enhavon de README.md en listaĵo de dosieroj">📜 readme', "ct_idxh": 'montri paĝon index.html anstataŭ listaĵo de dosieroj">htm', "ct_sbars": 'montri rulumskalojn">⟊', "cut_umod": "se dosiero jam ekzistas en la servilo, ŝanĝi la tempon de lasta modifo laŭ via dosiero (bezonas permesojn write+delete)\">re📅igi", "cut_turbo": "rapidigi alŝutojn KOSTE DE TUTA KONTROLADO:$N$Nuzinda, se vi alŝutis grandegajn dosierojn, devis haltigi la alŝutadon, kaj nun volas daŭrigi ĝin rapidege$N$Nse ĉi tiu agordo estas ŝaltita, anstataŭ kontrolsumado, la servilo nur kontrolas, ĉu la grando de via kaj servila dosieroj estas samaj, kaj ne realŝutas dosierojn kun samaj grandoj$N$Npost ĉio finiĝis, vi devus malŝalti ĉi tiun agordon, do provi "alŝuti" la tiuj samaj dosieroj — la kontrolsumado rekomencos kaj ne realŝutos ion ajn, se la alŝutado vere sukcesis\">rapidega", "cut_datechk": "efektas nur se "rapidega" alŝutado estas ŝaltita$N$Nete plibonigas fidindon de kontrolado, per kontrolado de modifo-tempoj aldone al grandoj$N$Nteorie estas sufiĉe por detekti nefinigitajn aŭ difektitajn alŝutojn, sed ne estas kompleta alternativo por sen-"rapidega" kontrolado\">dato-kontrolo", "cut_u2sz": "grando (en MiBoj) de ĉiu alŝutanta ero; grandaj valoroj estas pli bonaj por longdistancaj konektoj, malgrandajn por malalt-kvalitaj konektoj", "cut_flag": "certigi, ke nur unu langeto alŝutas samttempe $N -- aliaj langetoj devas ankaŭ ŝalti ĉi tiun agordon $N -- nur funkcias por langetoj de sama domajno", "cut_az": "alŝuti dosierojn en alfabeta ordigo anstataŭ "plej malgrandaj unue"$N$Nalfabeta ordo igas pli simple vidi, ke okazis eraroj en la servilo, sed estas pli malrapida sur tre rapidaj konektoj (ekz. en la loka reto aŭ per fibrooptiko)", "cut_nag": "sciigi per operaciumo je fino de alŝutado$N(nur se ĉi tiu langeto de retumilo ne estas aktiva)", "cut_sfx": "sciigi per sono je fino de alŝutado$N(nur se ĉi tiu langeto de retumilo ne estas aktiva)", "cut_mt": "kontrolsumi dosierojn pli rapide per multaj fadenoj$N$Nuzas teknologio Web Worker, bezonas pli da labormemoro (maksimume 512 MiB)$N$Nplirapidigas https je 30%, http je 4.5x\">mf", "cut_wasm": "uzi WASM-modulon anstataŭ kontrolsumilaj funkcioj de retumilo; plirapidigas kontrolsumadon sur Chrome-bazitaj retumiloj, sed ankaŭ pli procesor-intensa; malnovaj versioj de Chrome havas difektojn, kiuj misuzas la tutan labormemoron kaj kraŝas la retumilon, se ĉi tiu agordo estas ŝaltita\">wasm", "cft_text": "teksto de retpaĝsimbolo (blankigu kaj reŝargu la paĝon por malŝalti)", "cft_fg": "teksta koloro", "cft_bg": "fona koloro", "cdt_lim": "maks. nombro de dosieroj por montri en dosierujo", "cdt_ask": "je malsupro de paĝo, peti por ago$Nanstataŭ ŝarĝi pli da dosieroj", "cdt_hsort": "`kiom da ordigo-reguloj (`,sorthref`) inkludi en adreso de la paĝo. Se agordita kiel 0, reguloj, inkluditaj en la adreso, estos ignoritaj", "cdt_ren": "ebligi propran dekstra-klakan menuon, la normala menuo restas alirebla per MAJ + dekstra klako\">ŝalti", "cdt_rdb": "montri la normalan dekstraklakan menuon, kiam la propra jam estas malfermita kaj oni denove dekstre klakas\">duobla", "tt_entree": "montri arbovidan navig-panelon$NFulmoklavo: B", "tt_detree": "montri paĝnivelan navig-panelon$NFulmoklavo: B", "tt_visdir": "rulumi al elektita dosierujo", "tt_ftree": "baskuli dosieruj-arban aŭ teksto-dosieran vidon$NFulmoklavo: V", "tt_pdock": "fiksi patrajn dosierojn sur supro de panelo", "tt_dynt": "aŭtomate pligrandigi panelon", "tt_wrap": "linifaldo", "tt_hover": "montri kompletajn nomojn sur musumo$N( paneas rulumadon, se la kursoro de muso $N  ne estas en la maldekstra malplenaĵo )", "ml_pmode": "je la fino de dosierujo...", "ml_btns": "komandoj", "ml_tcode": "transkodi", "ml_tcode2": "transkodi al", "ml_tint": "kolorado", "ml_eq": "ekvalizilo", "ml_drc": "kompresoro", "ml_ss": "preterpasi silenton", "mt_loop": "ripeti unu kanton\">🔁", "mt_one": "haltigi post unu kanto\">1️⃣", "mt_shuf": "ludi ĉiu dosierujo en hazarda ordo\">🔀", "mt_aplay": "ludi aŭtomate, se ligilo enhavas identigilon de kanto$N$Nmalŝaltado de ĉi tiu agordo ankaŭ malŝaltas ĝisdatigadon de paĝ-adreso, por ke ludado ne rekomenciĝas, se la paĝo estos poste malfermita sen aliaj agordoj\">a▶", "mt_preload": "komenci ŝargadon de sekva kanto antaŭ la fino de la nuna, por kontinua ludado\">antaŭŝarg.", "mt_prescan": "eniri la sekvan dosierujon antaŭ la fino de la lasta kanto, $Npor ke la retumilo ne interrompis la ludadon\">nav", "mt_fullpre": "antaŭŝargi la tutan kanton;$N✅ ŝalti por malaltkvalitaj konektoj,$N❌ eble malŝalti por malrapidaj konektoj\">tute", "mt_fau": "por poŝtelefonoj: komenci sekvan kanton, eĉ se ĝi ne estis tute ŝargita (povas difektigi la montradon de muzikaĵ-etikedoj)\">☕️", "mt_waves": "bildigo:$Nmontri amplitudon de ludanta kanto en ludadbreto\">~", "mt_npclip": "montri butonojn por kopiado de ludanta kanto\">/np", //ne tradukita: "/np" estas referenco al komando, uzata en IRC-babilejoj "mt_m3u_c": "montri butonojn por kopiado de elektitaj kantoj kiel m3u8-ludlisto\">📻", "mt_octl": "integrado kun operaciumo (medio-klavoj kaj montriloj)\">integr.", "mt_oseek": "movi tra kanto per operaciumaj stiriloj$N$Nnoto: en iuj komputiloj (iPhone),$N ĉi tiu agordo anstataŭigas la butonon de sekva kanto\">movado", "mt_oscv": "montri album-bildojn en montriloj\">bildo", "mt_follow": "rulumi la pagon, por ke la ludanta kanto restas videbla\">🎯", "mt_compact": "kompaktaj ruliloj\">⟎", "mt_uncache": "malplenigi kaŝmemoron  (uzinda, se via retumilo kaŝmemoris$Ndifektitan kopion de kanto, kaj ne povas ludi ĝin)\">🗑️ kaŝmem.", "mt_mloop": "ripeti la nunan dosierujon\">🔁 ripeti", "mt_mnext": "ŝargi la sekvan dosierujon kaj daŭrigi\">📂 sekva", "mt_mstop": "haltigi ludadon\">⏸ haltigi", "mt_cflac": "konverti el flac / wav al {0}\">flac", "mt_caac": "konverti el aac / m4a al {0}\">aac", "mt_coth": "konverti aliajn (krom mp3) al {0}\">aliaj", "mt_c2opus": "pli bona elekto por propraj komputiloj kaj Android\">opus", "mt_c2owa": "opus-weba, por iOS 17.5 kaj pli novaj\">owa", "mt_c2caf": "opus-caf, por iOS 11-17\">caf", "mt_c2mp3": "por tre malnovaj iloj\">mp3", "mt_c2flac": "plej bona sonkvalito, sed grandegaj elŝutoj\">flac", "mt_c2wav": "sendensigita ludado (pli bonaj elŝutoj)\">wav", "mt_c2ok": "bona elekto", "mt_c2nd": "ĉi tiu formato ne estas rekomendita por via aparato, sed ĝi ankaŭ funkcios", "mt_c2ng": "via aparato ŝajne ne subtenas ĉi tiun formaton, sed ni provu uzi ĝin malgraŭe", "mt_xowa": "estas difektoj en iOS, kiuj preventas fonan ludadon per ĉi tiu formato; bonvolu uzi caf aŭ mp3 anstataŭe", "mt_tint": "travideblo (0-100) de ludadbreto$Nvi povas ŝanĝi ĝin, se ĝi aspektas tro distre dum ŝargado", "mt_eq": "`ŝaltas ekvalizilon kaj stirilon de plifortigado;$N$Nboost (plifortigado) `0` = senmodifa 100%a laŭteco$N$Nwidth (larĝo) `1  ` = senmodifa dukanala sono$Nwidth (larĝo) `0.5` = 50% miksado inter maldekstra kaj dekstra kanaloj$Nwidth (larĝo) `0  ` = unukanala sono$N$Nboost `-0.8` & width `10` = senvokigo :^)$N$Nŝaltita ekvalizilo ankaŭ forigas paŭzojn inter muzikaĵoj en senpaŭzaj albumoj, agordi ĉion kiel 0 (sed 'width' kiel 1), se vi volas nur tion", "mt_drc": "ŝaltas kompresoron de dinamiko (glatigas laŭtecon de muzikaĵoj); ankaŭ ŝaltas ekvalizilon, do agordu ĉion (sed 'width') kiel 0, se vi ne volas ĝin; $N$Nplimalgrandigas laŭtecon de aŭdio super sojlo-valoro ('tresh') da dB; ĉiu proporcio-valoro ('ratio') da dB post 'tresh' 1 dB estos eligita, do implicitaj valoroj (tresh = -24, ratio = 12) faras, ke laŭteco neniam pli grandas ol -22 dB; tiel estas sendanĝera agordi 'boost'on kiel 0.8 aŭ eĉ 1.8 dum ATK = 0 kaj grandega RLS, kiel 90 (funkcias nur en Firefox, RLS estas maksimume 1 en aliaj retumiloj)$N$N(rigardu vikipedion, ĝi klariĝas pli bone)", "mt_ss": "`ebligas transsalti silenton; multobligas la ludrapidon per `×rapid` ĉe komenco/fino de aŭdiaĵoj, kiam la laŭteco estas sub `laŭt` kaj la pozicio estas ene de la komencaj `ek%` aŭ finaj `fin%`", "mt_ssvt": "maksimuma laŭtnivelo por silentsaltado (0-255)\">laŭt", "mt_ssts": "komenca intervalo por silentsaltado (% de trakolongo)\">ek%", "mt_sste": "fina intervalo por silentsaltado (% de trakolongo)\">fin%", "mt_sssm": "multiplikado de ludrapideco dum silentsaltado\">×rapid", "mb_play": "ludi", "mm_hashplay": "ludi ĉi tiun aŭdiodosieron?", "mm_m3u": "premu Enter/OK por ludado \npremu ESK/Rezigni por redaktado", "mp_breq": "bezonas Firefox 82+, Chrome 73+ aŭ iOS 15+", "mm_bload": "ŝargado...", "mm_bconv": "konvertado al formato {0}, bonvolu atendi...", "mm_opusen": "via retumilo ne povas ludi dosierojn de formatoj aac / m4a;\ntranskodado al opus estas ŝaltigita", "mm_playerr": "ludado malsukcesis: ", "mm_eabrt": "Klopodo de ludado estis nuligita", "mm_enet": "Via retkonekto estas nestabila", "mm_edec": "Ĉi tiu dosiero estas ŝajne difektita??", "mm_esupp": "Via retumilo ne komprenas ĉi tiun aŭdio-formaton", "mm_eunk": "Nekonata eraro", "mm_e404": "Ne povas ludi aŭdiaĵon; eraro 404: Dosiero ne trovita.", "mm_e403": "Ne povas ludi aŭdiaĵon; eraro 403: Atingo malpermesita.\n\nKlopodu reŝargi paĝon per klavo F5, eble via seanco senvalidiĝis", "mm_e415": "Ne povas ludi aŭdiaĵon; eraro 415: Transkodigo de dosiero malsukcesis; rigardu la protokolojn de la servilo.", "mm_e500": "Ne povas ludi aŭdiaĵon; eraro 500: Rigardu la protokolojn de servilo.", "mm_e5xx": "Ne povas ludi aŭdiaĵon; servila eraro ", "mm_nof": "neniuj aŭdio-dosieroj trovitaj proksime", "mm_prescan": "Serĉado por sekva aŭdiaĵo...", "mm_scank": "Sekva muzikaĵo trovita:", "mm_uncache": "kaŝmemoro malplenigita; ĉiuj muzikaĵoj estos reelŝutitaj dum sekva ludado", "mm_hnf": "ĉi tiu muzikaĵo ne ekzistas plu", "im_hnf": "ĉi tiu bildo ne ekzistas plu", "f_empty": 'ĉi tiu dosierujo estas malplena', "f_chide": 'ĉi tiu ago kaŝos kolumnon «{0}»\n\nvi povas malkaŝi kolumnojn en agordoj', "f_bigtxt": "ĉi tiu dosiero estas {0}-MiB-granda -- ĉu vere malfermi kiel teksto?", "f_bigtxt2": "ĉu malfermi nur la finon de dosiero? ĉi tiu reĝimo ankaŭ ŝaltos tujan ĝisdatigon, novaj linioj estos tuj montritaj", "fbd_more": '
          {0} de {1} dosieroj montrataj; montri {2}montri ĉiujn
          ', "fbd_all": '
          {0} de {1} dosieroj montrataj; montri ĉiujn
          ', "f_anota": "nur {0} de {1} eroj estis elektita;\nrulumi al la malsupro por elekti la tutan dosierujon", "f_dls": 'la ligiloj de dosieroj en ĉi tiu dosierujo estis\nanstataŭigitaj per elŝuto-ligiloj', "f_dl_nd": 'dosierujo preterlasita (uzu zip/tar-elŝuton anstataŭe):\n', "f_partial": "Por sendifekta elŝuto de nune-alŝutata dosiero, elektu dosieron kun sama nomo, sed sen etendaĵo .PARTIAL. Bonvolu uzi la butonon \"Rezigni\" aŭ klavon ESK por fari tion.\n\nSe vi uzas OK / Enter, la provizora dosiero .PARTIAL estos elŝutita, kiu tre probable enhavas nekompletajn datumojn.", "ft_paste": "alglui {0} erojn$NFulmoklavo: stir-V", "fr_eperm": 'ne povas alinomi:\nvi ne havas permeson “move” en ĉi tiu dosierujo', "fd_eperm": 'ne povas forigi:\nvi ne havas permeson “delete” en ĉi tiu dosierujo', "fc_eperm": 'ne povas eltondi:\nvi ne havas permeson “move” en ĉi tiu dosierujo', "fp_eperm": 'ne povas alglui:\nvi ne havas permeson “write” en ĉi tiu dosierujo', "fr_emore": "elekti almenaŭ unu aĵon por alinomi", "fd_emore": "elekti almenaŭ unu aĵon por forigi", "fc_emore": "elekti almenaŭ unu aĵon por eltondi", "fcp_emore": "elekti almenaŭ unu aĵon por kopii al tondujo", "fs_sc": "kunhavigi la aktualan dosierujon", "fs_ss": "kunhavigi la elektitajn dosierojn", "fs_just1d": "vi ne povas elekti pli ol unu dosierujon\naŭ miksi dosierojn kaj dosierujojn en elektaĵo", "fs_abrt": "❌ ĉesigi", "fs_rand": "🎲 haz. nomo", "fs_go": "✅ krei komunaĵon", "fs_name": "nomo", "fs_src": "indiko", "fs_pwd": "pasvorto", "fs_exp": "tempolimo", "fs_tmin": "min", "fs_thrs": "horoj", "fs_tdays": "tagoj", "fs_never": "eterna", "fs_pname": "nomo de ligilo; estos hazarde kreita, se malplena", "fs_tsrc": "dosier(uj)o por kunhavigi", "fs_ppwd": "pasvorto (nedeviga)", "fs_w8": "kreado de komunaĵo...", "fs_ok": "premu Enter/OK por kopii al tondujo\npremu ESK/Rezigni por fermi", "frt_dec": "povas ripari difektitajn dosiernomojn\">url-malkodo", "frt_rst": "reagordi modifitajn dosiernomojn al originalaj\">↺ malfari", "frt_abrt": "ĉesigi operacion kaj fermi ĉi tiun fenestron\">❌ rezigni", "frb_apply": "ALINOMI", "fr_adv": "amasa / metadatuma / ŝablona alinomado\">altnivela", "fr_case": "uskleciva regula esprimo\">uskleco", "fr_win": "Windows-taŭgaj nomoj; signoj <>:"\\|?* estos anstataŭigitaj per japanaj duobla-larĝaj signoj\">win", "fr_slash": "anstataŭigi /n per signo, kiu ne devigas kreadon de novaj dosierujoj\">sen /", "fr_re": "`ŝablono de regula esprimo, kiu estos aplikita al originalaj dosiernomoj; kaptogrupoj povas esti referencita en formatkampo, ekz. `(1)`, `(2)` k.t.p.", "fr_fmt": "`inspirita de foobar2000:$N`(title)` anstataŭigitas per nomo de muzikaĵo,$N`[(artist) - ](title)` preterpasas [ĉi tiun] parton, se artisto ne estas specifita$N`$lpad((tn),2,0)` aldonas nulojn en trakonombro ĝis 2 ciferoj", "fr_pdel": "forigi", "fr_pnew": "konservi kiel", "fr_pname": "nomu vian novan ŝablonon", "fr_aborted": "ĉesigita", "fr_lold": "malnova nomo", "fr_lnew": "nova nomo", "fr_tags": "etikedoj por elektitaj dosieroj (ne redakteblas, nur por referenco):", "fr_busy": "alinomado de {0} aĵoj...\n\n{1}", "fr_efail": "alinomado malsukcesis:\n", "fr_nchg": "{0} da novaj nomoj estis modifita pro reguloj win kaj/aŭ sen /\n\nĈu daŭrigi kun modifitaj nomoj?", "fd_ok": "forigado sukcesis", "fd_err": "forigado malsukcesis:\n", "fd_none": "nenio estis forigita; eble servila eraro malpermesis ĝin (xbd)?", "fd_busy": "forigado de {0} aĵoj...\n\n{1}", "fd_warn1": "ĉu FORIGI ĉi tiujn {0} aĵojn?", "fd_warn2": "Averto! Ĉi tiu ago ne malfareblas. Ĉu forigi?", "fc_ok": "{0} aĵoj eltonditaj", "fc_warn": '{0} aĵoj eltonditaj\n\nnur ĉi tiu langeto de retumilo povas alglui ilin\n(pro la grando de elektaĵo)', "fcc_ok": "{0} aĵoj kopiitaj al tondujo", "fcc_warn": '{0} aĵoj kopiitaj al tondujo\n\nnur ĉi tiu langeto de retumilo povas alglui ilin\n(pro la grando de elektaĵo)', "fp_apply": "uzi ĉi tiujn nomojn", "fp_skip": "preterpasi konfliktojn", "fp_ecut": "unue eltondi aŭ kopii dosier(uj)ojn, do alglui ĝin poste\n\nnoto: tondujo ankaŭ funkcias inter aliaj langetoj de retumilo", "fp_ename": "{0} aĵoj ne povas esti movitaj, ĉar iliaj nomoj estas jam uzataj. Alinomi ilin sube aŭ lasi la nomokampojn malplenaj (\"preterpasi konfliktojn\") por preterpasi:", "fcp_ename": "{0} aĵoj ne povas esti kopiitaj, ĉar iliaj nomoj estas jam uzataj. Alinomi ilin sube aŭ lasi la nomokampojn malplenaj (\"preterpasi konfliktojn\") por preterpasi:", "fp_emore": "ankoraŭ restas koincidoj de dosiernomoj, kiuj bezonas solvon", "fp_ok": "movado sukcesis", "fcp_ok": "kopiado sukcesis", "fp_busy": "movado de {0} aĵoj...\n\n{1}", "fcp_busy": "kopiado {0} aĵoj...\n\n{1}", "fp_abrt": "ĉesigado...", "fp_err": "movado malsukcesis:\n", "fcp_err": "kopiado malsukcesis:\n", "fp_confirm": "ĉu movi tiujn {0} aĵojn ĉi tien?", "fcp_confirm": "ĉu kopii tiujn {0} aĵojn ĉi tien?", "fp_etab": 'eraro dum legado de tondujo el alia langeto de retumilo', "fp_name": "alŝutado de dosiero el via aparato. Nomi ĝin:", "fp_both_m": '
          elektu, kion alglui
          Enter = Movi {0} dosierojn al «{1}»\nESK = Alŝuti {2} dosierojn el via aparato', "fcp_both_m": '
          elektu, kion alglui
          Enter = Kopii {0} dosierojn al «{1}»\nESK = Alŝuti {2} dosierojn el via aparato', "fp_both_b": 'MoviAlŝuti', "fcp_both_b": 'KopiiAlŝuti', "mk_noname": "tajpu nomon en tekstokampo maldekstre antaŭ vi faras ĉi tion :p", "nmd_i1": "vi povas aldoni la deziratan sufikson, ekzemple .md", "nmd_i2": "vi povas krei nur .{0}-dosierojn, ĉar vi ne rajtas forigi dosierojn", "tv_load": "Ŝargado de teksto-dokumento:\n\n{0}\n\n{1}% ({2} da {3} MiB ŝargita)", "tv_xe1": "ne povas ŝargi teksto-dosieron:\n\neraro ", "tv_xe2": "404, dosiero ne trovita", "tv_lst": "listo de teksto-dosieroj en", "tvt_close": "reveni al vido de dosierujo$NFulmoklavo: M (aŭ Esk)\">❌ fermi", "tvt_dl": "elŝuti ĉi tiun dosieron$NFulmoklavo: Y\">💾 elŝuti", "tvt_prev": "montri malsekvan dokumenton$NFulmoklavo: i\">⬆ malsekva", "tvt_next": "montri sekvan dokumenton$NFulmoklavo: K\">⬇ sekva", "tvt_sel": "elekti dosieron   ( por eltondado / kopiado / forigado / ... )$NFulmoklavo: S\">elekti", "tvt_j": "beligi JSONon$NFulmoklavo: MAJ-J\">j", "tvt_edit": "malfermi dosieron en teksto-redaktilo$NFulmoklavo: E\">✏️ redakti", "tvt_tail": "observi ŝanĝojn en dosiero; novaj linioj estos tuje montritaj\">📡 gvati", "tvt_wrap": "linifaldo\">↵", "tvt_atail": "alpingli rulumadon al malsupro de paĝo\">⚓", "tvt_ctail": "malkodi ANSI-kodojn de terminal-koloroj\">🌈", "tvt_ntail": "limo de rulumado (kiom da bajtoj de teksto konservi en memoro)", "m3u_add1": "muzikaĵo aldonita al m3u-ludlisto", "m3u_addn": "{0} muzikaĵoj aldonitaj al m3u-ludlisto", "m3u_clip": "m3u-ludlisto kopiita al tondujo\n\nvi devus krei tekst-dosieron kun etendaĵo .m3u kaj alglui la tekston en ĝi por krei uzeblan ludliston", "gt_vau": "ne montri videojn, nur ludi muzikaĵojn\">🎧", "gt_msel": "ŝalti elektado-reĝimon; stir-klaki dosieron por ne-elekta ago$N$N<em>kiam ŝaltita: duoblaklako por malfermi dosier(uj)on</em>$N$NFulmoklavo: S\">elektado", "gt_crop": "stuci bildetojn\">stuci", "gt_3x": "alt-kvalitaj bildetoj\">3x", "gt_zoom": "grando", "gt_chop": "nomlongo", "gt_sort": "ordigi per", "gt_name": "nomo", "gt_sz": "grando", "gt_ts": "dato", "gt_ext": "tipo", "gt_c1": "pli mallongaj dosiernomoj", "gt_c2": "pli longaj dosiernomoj", "sm_w8": "serĉado...", "sm_prev": "serĉrezultoj sube venas al la lasta informpeto:\n ", "sl_close": "fermi serĉrezultojn", "sl_hits": "montrado de {0} kongruaĵoj", "sl_moar": "ŝargi pli", "s_sz": "grando", "s_dt": "dato", "s_rd": "vojo", "s_fn": "nomo", "s_ta": "etikedoj", "s_ua": "alŝut📅", "s_ad": "aliaj", "s_s1": "minimuma MiB", "s_s2": "maksimuma MiB", "s_d1": "min. iso8601", "s_d2": "maks. iso8601", "s_u1": "alŝutita post", "s_u2": "kaj/aŭ antaŭ", "s_r1": "vojo enhavas   (apartigi per spacoj)", "s_f1": "nomo enhavas   (negacii per minuso)", "s_t1": "etikedoj enhavas   (^=komenco, fino=$)", "s_a1": "ecoj de metadatumoj", "md_eshow": "ne povas montri ", "md_off": "[📜readme] malŝaltita en [⚙️] -- dokumento kaŝita", "badreply": "Eraro dum legado de respondo al servilo", "xhr403": "403: Atingo malpermesita\n\nklopodu reŝargi paĝon per klavo F5, eble via seanco senvalidiĝis", "xhr0": "nekonata (eble konekto al servilo estis perdita, aŭ servilo ne funkcias)", "cf_ok": "pardonon -- DD" + wah + "oS-protekto aktiviĝis\n\nĉio devus esti bona 30 sekundoj post\n\nse nenio okazas, reŝargi per klavo F5", "tl_xe1": "ne povas listigi subdosierujojn:\n\neraro ", "tl_xe2": "404: Dosierujo ne trovita", "fl_xe1": "ne povas listigi dosierojn en dosierujo:\n\neraro ", "fl_xe2": "404: Dosierujo ne trovita", "fd_xe1": "ne povas krei subdosierujon:\n\neraro ", "fd_xe2": "404: Patra dosierujo ne trovita", "fsm_xe1": "ne povas sendi mesaĝon:\n\neraro ", "fsm_xe2": "404: Patra dosierujo ne trovita", "fu_xe1": "ne povas ŝargi malsend-liston el servilo:\n\neraro ", "fu_xe2": "404: Dosiero ne trovita??", "fz_tar": "nedensigita dosiero GNU TAR (linux / mac)", "fz_pax": "nedensigita PAX-formata dosiero TAR (malpli rapide)", "fz_targz": "GNU TAR, densigita per 3a nivelo de GZip$N$Nkutime tre malrapida, do$Nuzu nedensigitan TARon anstataŭe", "fz_tarxz": "GNU TAR, densigita per 1a nivelo de XZ$N$Nkutime tre malrapida, do$Nuzu nedensigitan TARon anstataŭe", "fz_zip8": "Zip kun dosiernomoj laŭ UTF-8 (eble difektiĝis je Windows 7 kaj pli malnovaj)", "fz_zipd": "Zip kun dosiernomoj laŭ CP437, por tre malnovaj programoj (esperantaj diakritaĵoj ne funkcios)", "fz_zipc": "Zip, cp437, CRC-kontrolsumoj prekalkulitaj,$Npor MS-DOS PKZIP v2.04g (oktobro 1993)$N(bezonas pli da tempo antaŭ komenco de elŝuto)", "un_m1": "vi povas forigi viajn lastajn alŝutojn (aŭ ĉesigi nefinigitajn) sube", "un_upd": "reŝargi", "un_m4": "aŭ kunhavigi la dosierojn sube:", "un_ulist": "montri", "un_ucopy": "kopii", "un_flt": "nedeviga filtrilo:  URL devas enhavi", "un_fclr": "vakigi filtrilon", "un_derr": 'malalŝutado malsukcesis:\n', "un_f5": 'io difektiĝis, bonvolu reŝargi aŭ uzi klavon F5', "un_uf5": "pardonu, sed vi devas reŝargi la paĝon (F5 aŭ Stir+R) antaŭ ĉesigi ĉi tiun alŝuton", "un_nou": 'averto: servilo estas tro okupara por montri nefinigitajn alŝutojn; klaku la butonon "reŝargi" post kelkaj sekundoj', "un_noc": 'averto: malalŝutado de tute alŝutitaj dosieroj estas malpermesita laŭ agordoj de servilo', "un_max": "unuaj 2000 dosieroj montritaj (uzi filtrilon por vidi aliajn)", "un_avail": "{0} lastaj alŝutoj forigeblas
          {1} nefinigitajn ĉesigeblas", "un_m2": "ordigita per alŝuto-tempo, plej lastaj unue:", "un_no1": "neniuj sufiĉe lastaj alŝutoj", "un_no2": "neniuj sufiĉe lastaj alŝutoj, kongruaj laŭ filtrilo", "un_next": "forigi la sekvajn {0} dosierojn sube", "un_abrt": "ĉesigi", "un_del": "forigi", "un_m3": "ŝargado de viaj lastaj alŝutoj...", "un_busy": "forigado de {0} dosieroj...", "un_clip": "{0} ligiloj kopiitaj al tondujo", "u_https1": "vi devas", "u_https2": "ŝalti HTTPS-protokolon", "u_https3": "por pli bona rendimento", "u_ancient": 'via retumilo estas vere antikva -- eble vi devus uzi alŝutilon bup anstataŭe', "u_nowork": "Firefox 53+ aŭ Chrome 57+ aŭ iOS 11+ necesas", "tail_2old": "Firefox 105+ aŭ Chrome 71+ aŭ iOS 14.5+ necesas", "u_nodrop": 'via retumilo estas tro malnova por ŝova-kaj-demeta alŝutado', "u_notdir": "tio ne estas dosierujo!\n\nvia retumilo estas tro malnova,\nbonvolu ŝovu kaj demetu anstataŭe", "u_uri": "por ŝovi-kaj-demeti bildon de aliaj fenestroj de retumiloj,\nbonvolu demeti ĝin sur la grandan alŝut-butonon", "u_enpot": 'uzi simplan fasadon (povas plirapidigi alŝutojn)', "u_depot": 'uzi elegantan fasadon (povas plimalrapidigi alŝutojn)', "u_gotpot": 'ŝaltado de simpla fasado por pli rapidaj alŝutoj,\n\nvi povas malŝalti ĝin, se ĝi ne plaĉas al vi!', "u_pott": "

          dosieroj:   {0} finitaj,   {1} eraroj,   {2} alŝutataj,   {3} envicigitaj

          ", "u_ever": "ĉi tiu estas fundamenta alŝutilo; up2k postulas almenaŭ retumilojn
          Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1", "u_su2k": 'ĉi tiu estas fundamenta alŝutilo; up2k estas pli bona', "u_uput": 'optimumigi por rapideco (ne kalkuli kontrolsumojn)', "u_ewrite": 'vi ne havas permeson skribi en ĉi tiun dosierujon', "u_eread": 'vi ne havas permeson legi ĉi tiun dosierujon', "u_enoi": 'serĉado de dosieroj estas malŝaltita en servilaj agordoj', "u_enoow": "anstataŭigo ne funkcios ĉi tie; forigo-permeso necesas", "u_badf": 'Ĉi tiuj {0} de {1} dosieroj estis preterpasitaj, eble pro permesoj de dosiersistemo:\n\n', "u_blankf": 'Ĉi tiuj {0} de {1} dosieroj estas blankaj; ĉu alŝuti malgraŭ tio?\n\n', "u_applef": 'Ĉi tiuj {0} de {1} dosieroj estas eble nedezirataj;\nPremu OK/Enter por PRETERPASI ilin,\nPremu Rezigni/ESK por ignori ĉi tiun mesaĝon kaj ALŜUTI ilin:\n\n', "u_just1": '\nEble ĝi funkcios pli bone, se vi elektas nur unu dosieron', "u_ff_many": "se vi uzas operaciumojn Linux / MacOS / Android, ĉi tiu kvanto de dosieroj povas kraŝi retumilon Firefox!\nse ĉi tio okazos, klopodu denove (aŭ uzu Chrome-on).", "u_up_life": "Ĉi tiu alŝuto estos forigita de servilo\n{0} post kompletado", "u_asku": 'alŝuti ĉi tiujn {0} dosierojn al {1}', "u_unpt": "vi povas malalŝuti / forigi ĉi tiun alŝuton per 🧯 supre-maldesktre", "u_bigtab": '{0} dosieroj montrotaj\n\nĉi tiu povas kraŝi vian retumilon, ĉu daŭrigi?', "u_scan": 'Skanado de dosieroj...', "u_dirstuck": 'skanilo haltis dum atingado de sekvaj {0} dosieroj; ili estos preterpasitaj:', "u_etadone": 'Finita ({0}, {1} dosieroj)', "u_etaprep": '(preparado por alŝutado)', "u_hashdone": 'kontrolsumita', "u_hashing": 'k-sumado', "u_hs": 'kvitanco...', "u_started": "la dosieroj estas alŝutataj; rigardu [🚀]", "u_dupdefer": "duplikatoj; estos traktita post ĉiuj aliaj dosieroj", "u_actx": "alklaku ĉi tiun tekston por eviti malrapidigon
          dum uzado de aliaj fenestroj/langetoj", "u_fixed": "Bone!  Riparita 👍", "u_cuerr": "alŝutado de ero {0} de {1} malsukcesis;\neble malgravas, alŝutado daŭrigas\n\ndosiero: {2}", "u_cuerr2": "servilo rifuzis alŝutadon (ero {0} de {1});\nprovos denove poste\n\ndosiero: {2}\n\neraro ", "u_ehstmp": "reprovos poste; rigardu sube-dekstre", "u_ehsfin": "servilo rifuzis peton por finigi alŝutadon; reprovado...", "u_ehssrch": "servilo rifuzis peton por ŝerco; reprovado...", "u_ehsinit": "servilo rifuzis peton por komenco de alŝutado; reprovado...", "u_eneths": "reta eraro dum alŝutada kvitanco; reprovado...", "u_enethd": "reta eraro dum kontrolo de ekzistado de cela dosiero; reprovado...", "u_cbusy": "atendado, por ke la servilo estas atingebla post reta eraro...", "u_ehsdf": "servilo ne havas sufiĉe da diskospaco!\n\nla alŝutado reprovos daŭre, okaze de iu liberigas\nsufiĉe da diskospaco", "u_emtleak1": "ŝajnas, ke via retumilo likas memoron; \nbonvolu", "u_emtleak2": ' uzi protokolon HTTPS (rekomendita) aŭ ', "u_emtleak3": ' ', "u_emtleakc": 'provu fari tiel:\n
          • reŝargu la paĝon per klavo F5
          • , do malŝalti la butonon  mt  en la  ⚙️ agordoj
          • kaj reprovu alŝuton
          Alŝuto estos pli malrapida, sed nenio fareblas.\nPardonon por la ĝenaĵo!\n\nPS: chrome v107 korektis ĉi tion', "u_emtleakf": 'provu fari tiel:\n
          • reŝargu la paĝon per klavo F5
          • , do malŝalti la butonon  mt  en la  ⚙️ agordoj
          • kaj reprovu alŝuton
          \nPS: firefox espereble korektos ĉi tion baldaŭ', "u_s404": "ne trovita ĉe la servilo", "u_expl": "klarigi", "u_maxconn": "plimulto da retumiloj ne permesas uzi pli ol 6, sed Firefox havas agordon connections-per-server en about:config, per kiu la limo ŝanĝeblas", "u_tu": '

          AVERTO: rapidega reĝimo ŝaltita,  kliento povas ne detekti kaj daŭrigi nefinigitajn alŝutojn; rigardu la ŝpruchelpilon de rapidega-butono

          ', "u_ts": '

          AVERTO: rapidega reĝimo ŝaltita,  serĉrezultoj povas esti eraraj; rigardu la ŝpruchelpilon de rapidega-butono

          ', "u_turbo_c": "rapidega reĝimo estas malpermesita laŭ servilaj agordoj", "u_turbo_g": "rapidega reĝimo estas malpermesita, ĉar vi\nne rajtas listigi dosierujojn en ĉi tiu datumportilo", "u_life_cfg": 'aŭtoforigado post minutoj (aŭ horoj)', "u_life_est": 'alŝuto estos forigita je ---', "u_life_max": 'ĉi tiu dosierujo postulas \naŭtoforigadon de dosieroj post {0}', "u_unp_ok": 'malalŝuto permesitas por {0}', "u_unp_ng": 'malalŝuto NE estos permesita', "ue_ro": 'via atingo de ĉi tiu dosierujo estas nur-lega\n\n', "ue_nl": 'vi ne estas ensalutita', "ue_la": 'vi estas ensalutita kiel "{0}"', "ue_sr": 'vi estas en serĉo-reĝimo nun\n\nŝanĝi al alŝuto-reĝimo per lupeo-simbolo 🔎 (apude de la grandega SERĈO-butono), kaj provu alŝuti denove\n\npardonon', "ue_ta": 'provu alŝuti denove, ĝi devus funkcii nun', "ue_ab": "ĉi tiu dosiero estas jam alŝutata en alian dosierujon, tiu alŝutado devas esti finigita antaŭ alŝutado de la sama dosiero en alian lokon.\n\nVi povas ĉesigi la nunan alŝutadon per butonon 🧯 supre-maldekstre", "ur_1uo": "OK: Dosiero sukcese alŝutita", "ur_auo": "OK: Ĉiuj {0} dosieroj sukcese alŝutitaj", "ur_1so": "OK: Dosiero trovita ĉe la servilo", "ur_aso": "OK: Ĉiuj {0} dosieroj trovitaj ĉe la serviloj", "ur_1un": "Alŝutado malsukcesis, pardonon", "ur_aun": "Ĉiuj {0} alŝutadoj malsukcesis, pardonon", "ur_1sn": "Dosiero estis NE trovita ĉe la servilo", "ur_asn": "La {0} dosieroj estis NE trovitaj ĉe la servilo", "ur_um": "Finita;\n{0} alŝutoj sukcesis,\n{1} alŝutoj malsukcesis, pardonon", "ur_sm": "Finita;\n{0} dosieroj trovitaj ĉe la servilo,\n{1} dosieroj NE trovitaj ĉe la servilo", "rc_opn": "malfermi", "rc_ply": "ludi", "rc_pla": "ludi kiel aŭdiaĵo", "rc_txt": "malfermi per tekstovidilo", "rc_md": "malfermi per tekstoredaktilo", "rc_dl": "elŝuti", "rc_zip": "elŝuti kiel arkivo", "rc_cpl": "kopii ligilon", "rc_del": "forigi", "rc_cut": "eltondi", "rc_cpy": "kopii", "rc_pst": "alglui", "rc_rnm": "alinomi", "rc_nfo": "nova dosierujo", "rc_nfi": "nova dosiero", "rc_sal": "elekti ĉiujn", "rc_sin": "inversigi elekton", "rc_shf": "kunhavigi ĉi tiun dosierujon", "rc_shs": "kunhavigi elekton", "lang_set": "ĉu reŝargi paĝon por efektivigi lingvo-ŝanĝon?", "splash": { "a1": "reŝargi", "b1": "sal, nekonatulo   (vi ne estas ensalutita)", "c1": "elsaluti", "d1": "montru stakon", "d2": "montras la staton de ĉiuj aktivaj fadenoj", "e1": "reŝargi CFGon", "e2": "reŝargas la agordo-dosierojn (kontoj/portiloj/portilo-flagoj),$Nkaj reskanas ĉiuj portiloj de ed2s$N$Nnoto: ĉiuj ŝanĝoj de ĝeneralaj agordoj postulas$Npostulas tutan restartigon por efektiviĝi", "f1": "vi povas vidi:", "g1": "vi povas alŝuti al:", "cc1": "aliaĵoj:", "h1": "malŝalti k304-on", "i1": "ŝalti k304-on", "j1": "k304 malkonektas vian klienton je ĉiu HTTP-eraro 304; tio povas eviti paraliziĝon dum uzado de difektitaj retperantoj (paĝoj subite ne ŝargiĝas), sed ĝi ankaŭ plimalrapidigas ĉion", "k1": "rekomenci agordojn de kliento", "l1": "ensaluti por pli da opcioj:", "ls3": "ensaluti", "lu4": "uzantnomo", "lp4": "pasvorto", "lo3": "ensaluti kiel “{0}” ĉie", "lo2": "ĉi tiu finigos seancon en ĉiuj retumiloj", "m1": "bonvenon denove,", "n1": "404 ne trovita  ┐( ´ -`)┌", "o1": 'aŭ eble vi ne havas rajton -- provu uzi pasvorton aŭ iri hejmen', "p1": "403 ne permesita  ~┻━┻", "q1": 'uzu pasvorton aŭ iru hejmen', "r1": "hejmen", ".s1": "reskani", "t1": "ago", "u2": "tempo post lasta skribo (alŝuto / alinomado / ...) je servilo$N( upload / rename / ... )$N$N17d = 17 tagoj$N1h23 = 1 horo 23 minutoj$N4m56 = 4 minutoj 56 sekundoj", "v1": "konekti", "v2": "uzi ĉi tiun servilon kiel loka disko", "w1": "uzi HTTPS-protokolon", "x1": "ŝanĝi pasvorton", "y1": "redakti komunaĵojn", "z1": "malŝlosi ĉi tiun komunaĵon:", "ta1": "entajpu novan pasvorton unue", "ta2": "retajpu por konfirmi:", "ta3": "tajpo-eraro; bonvolu provu denove", "nop": "ERARO: Pasvorto ne povas esti malplena", "nou": "ERARO: Uzantnomo kaj/aŭ pasvorto ne povas esti malplena", "aa1": "aktivaj alŝutoj:", "ab1": "malŝalti no304-on", "ac1": "ŝalti no304-on", "ad1": "no304 malŝaltas ĉiun kaŝmemoradon; provu ĉi tion, se k304 ne riparis la difektojn. Ĉi tiu agordo malŝparas multon da datumtrafiko!", "ae1": "aktivaj elŝutoj:", "af1": "montri lastajn alŝutojn", "ag1": "montri kaŝmemoron de idp", } }; ================================================ FILE: copyparty/web/tl/fin.js ================================================ // Rivut, jotka päättyvät //m, ovat varmentamattomia konekäännöksiä Ls.fin = { "tt": "Suomi", "cols": { "c": "toimintopainikkeet", "dur": "kesto", "q": "laatu / bittinopeus", "Ac": "äänikoodekki", "Vc": "videokoodekki", "Fmt": "formaatti / säiliö", "Ahash": "äänen tarkistussumma", "Vhash": "videon tarkistussumma", "Res": "resoluutio", "T": "tiedostotyyppi", "aq": "äänenlaatu / bittinopeus", "vq": "kuvalaatu / bittinopeus", "pixfmt": "alinäytteistys / pikselirakenne", "resw": "horisontaalinen resoluutio", "resh": "vertikaalinen resoluutio", "chs": "äänikanavat", "hz": "näytteenottotaajuus", }, "hks": [ [ "misc", ["ESC", "sulje asioita"], "file-manager", ["G", "vaihda lista/kuvanäkymään"], ["T", "vaihda pienoiskuviin/kuvakkeisiin"], ["⇧ A/D", "pienoiskuvien koko"], ["ctrl-K", "poista valitut"], ["ctrl-X", "siirrä valitut leikepöydälle"], ["ctrl-C", "kopioi valitut leikepöydälle"], ["ctrl-V", "siirrä tai kopioi tähän"], ["Y", "lataa valitut"], ["F2", "uudelleennimeä valitut"], "file-list-sel", ["space", "vaihda tiedostonvalintatilaan"], ["↑/↓", "siirrä valintaosoitinta"], ["ctrl ↑/↓", "siirrä osoitinta ja näkymää"], ["⇧ ↑/↓", "valitse edellinen/seuraava tiedosto"], ["ctrl-A", "valitse kaikki tiedostot / hakemistot"], ], [ "navigation", ["B", "näytä linkkipolku"], ["I/K", "siirry edelliseen/seuraavaan hakemistoon"], ["M", "siirry ylähakemistoon/supista nykyinen hakemisto"], ["V", "näytä hakemistot/tekstitiedostot navigointipaneelissa"], ["A/D", "navigointipaneelin koko"], ], [ "audio-player", ["J/L", "edellinen/seuraava kappale"], ["U/O", "kelaa 10s taaksepäin/eteenpäin"], ["0..9", "siirry 0%..90%"], ["P", "toista/pysäytä kappale"], ["S", "valitse toistossa oleva kappale"], ["Y", "lataa kappale"], ], [ "image-viewer", ["J/L, ←/→", "edellinen/seuraava kuva"], ["Home/End", "ensimmäinen/viimeinen kuva"], ["F", "siirry koko näytön tilaan"], ["R", "kierrä myötäpäivään"], ["⇧ R", "kierrä vastapäivään"], ["S", "valitse kuva"], ["Y", "lataa kuva"], ], [ "video-player", ["U/O", "kelaa 10s taaksepäin/eteenpäin"], ["P/K/Space", "toista/pysäytä video"], ["C", "jatka toistoa seuraavaan videoon"], ["V", "toista uudelleen"], ["M", "vaimenna"], ["[ ja ]", "aseta videon uudelleentoistoväli"], ], [ "textfile-viewer", ["I/K", "edellinen/seuraava tiedosto"], ["M", "sulje tekstitiedosto"], ["E", "muokkaa tekstitiedostoa"], ["S", "valitse tiedosto (leikkausta/kopiointia/uudelleennimeämistä varten)"], ["Y", "lataa tekstitiedosto"], ["⇧ J", "muotoile/siisti json"], ] ], "m_ok": "OK", "m_ng": "Peruuta", "enable": "Aktivoi", "danger": "HUOMIO!", "clipped": "kopioitu leikepöydälle", "ht_s1": "sekunti", "ht_s2": "sekuntia", "ht_m1": "minuutti", "ht_m2": "minuuttia", "ht_h1": "tunti", "ht_h2": "tuntia", "ht_d1": "päivä", "ht_d2": "päivää", "ht_and": " ja ", "goh": "hallintapaneeli", "gop": 'viereinen hakemisto">edell', "gou": 'ylempi hakemisto">ylös', "gon": 'seuraava hakemisto">seur', "logout": "Kirjaudu ulos ", "login": "Kirjaudu sisään", "access": " -oikeudet", "ot_close": "sulje alavalikko", "ot_search": "`etsi tiedostoja ominaisuuksien, tiedostopolun tai -nimen, musiikkitägien tai näiden yhdistelmän perusteella$N$N`foo bar` = täytyy sisältää sekä «foo» että «bar»,$N`foo -bar` = täytyy sisältää «foo» mutta ei «bar»,$N`^yana .opus$` = alkaa «yana» ja on «opus»-tiedosto$N`"try unite"` = sisältää täsmälleen «try unite»$N$Npäivämäärän muoto on iso-8601, kuten$N`2009-12-31` tai `2020-09-12 23:30:00`", "ot_unpost": "unpost: poista viimeaikaiset tai keskeytä keskeneräiset lataukset", "ot_bup": "bup: tiedostojen 'perus'lähetysohjelma, tukee jopa netscape 4.0", "ot_mkdir": "mkdir: luo uusi hakemisto", "ot_md": "new-file: luo uusi tekstitiedosto", "ot_msg": "msg: lähetä viesti palvelinlokiin", "ot_mp": "mediasoittimen asetukset", "ot_cfg": "asetukset", "ot_u2i": 'up2k: lähetä tiedostoja (vaatii write-oikeudet) tai vaihda hakutilaan nähdäksesi, ovatko tiedostot jo olemassa jossain päin palvelinta$N$Nlatauksia voi jatkaa, ne ovat monisäikeistettyjä, ja tiedostojen aikaleimat säilytetään; nuijii prosessoria enemmän kuin [🎈]  (peruslatausohjelma)

          tiedostojen lähetyksen aikana tämä kuvake muuttuu kertoo lähetyksen edistymisestilanteen!', "ot_u2w": 'up2k: lähetä tiedostoja jatkamistoiminnolla (voit sulkea selaimen ja vetää samat tiedostot selainikkunaan myöhemmin)$N$monisäikeistetty, ja tiedostojen aikaleimat säilyvät; nuijii prosessoria enemmän kuin [🎈]  (peruslatausohjelma)

          tiedostojen lähetyksen aikana tämä kuvake muuttuu kertoo lähetyksen edistymisestilanteen!', "ot_noie": 'Suosittelemme käyttämään uudempaa selainta.', "ab_mkdir": "luo hakemisto", "ab_mkdoc": "luo tekstitiedosto", "ab_msg": "lähetä viesti palvelinlokiin", "ay_path": "siirry hakemistoihin", "ay_files": "siirry tiedostoihin", "wt_ren": "uudelleennimeä valitut kohteet$NPikanäppäin: F2", "wt_del": "poista valitut kohteet$NPikanäppäin: ctrl-K", "wt_cut": "siirrä valitut kohteet leikepöydälle <small>(siirtääksesi ne muualle)</small>$NPikanäppäin: ctrl-X", "wt_cpy": "kopioi valitut kohteet leikepöydälle$N(liittääksesi ne muualle)$NPikanäppäin: ctrl-C", "wt_pst": "liitä aiemmin leikatut / kopioidut valinnat$NPikanäppäin: ctrl-V", "wt_selall": "valitse kaikki tiedostot$NPikanäppäin: ctrl-A (kun tiedosto on kohdistettu)", "wt_selinv": "valitse vastakkaiset tiedostot", "wt_zip1": "lataa tämä hakemisto pakattuna", "wt_selzip": "lataa valitut kohteet pakattuna", "wt_seldl": "lataa valitut kohteet paketoimatta$NPikanäppäin: Y", "wt_npirc": "kopioi kappaletiedot IRC-muotoilulla", "wt_nptxt": "kopioi kappaletiedot ilman muotoilua", "wt_m3ua": "lisää m3u-soittolistaan (klikkaa 📻kopioi myöhemmin)", "wt_m3uc": "kopioi m3u-soittolista leikepöydälle", "wt_grid": "vaihda kuva- ja listanäkymän välillä$NPikanäppäin: G", "wt_prev": "edellinen kappale$NPikanäppäin: J", "wt_play": "toista / pysäytä$NPikanäppäin: P", "wt_next": "seuraava kappale$NPikanäppäin: L", "ul_par": "rinnakkaislatausten lkm:", "ut_rand": "satunnaisgeneroidut tiedostonimet", "ut_u2ts": "kopioi viimeksi muokattu aikaleima$Ntiedostojärjestelmästäsi palvelimelle\">📅", "ut_ow": "korvaa olemassa olevat tiedostot palvelimella?$N🛡️: ei koskaan (luo sen sijaan uuden tiedostonimen)$N🕒: korvaa jos palvelintiedosto on vanhempi kuin omasi$N♻️: korvaa aina jos tiedostot ovat erilaisia$N⏭️: ohita kaikki olemassa olevat tiedostot ehdottomasti", "ut_mt": "jatka muiden tiedostojen tiivisteiden laskemista latauksen aikana$N$Nkannattanee poistaa käytöstä, mikäli prosessori tai kovalevy on vanhempaa mallia", "ut_ask": 'kysy vahvistusta ennen latauksen aloittamista">💭', "ut_pot": "paranna latausnopeutta hitailla laitteilla$Nvähentämällä käyttöliittymän monimutkaisuutta", "ut_srch": "lataamisen sijaan tarkista, ovatko tiedostot jo $N olemassa palvelimella (käy läpi kaikki hakemistot, joihin sinulla on read-oikeudet)", "ut_par": "keskeytä lataukset asettamalla se nollaan$N$Nnosta, jos yhteytesi on hidas tai viive on suuri$N$Npidä se 1:ssä lähiverkossa tai jos palvelimen kovalevy on pullonkaula", "ul_btn": "vedä tiedostoja / hakemistoja tähän
          (tai klikkaa minua)", "ul_btnu": "L Ä H E T Ä", "ul_btns": "E T S I", "ul_hash": "tiiviste", "ul_send": "lähetä", "ul_done": "valmis", "ul_idle1": "ei latauksia jonossa", "ut_etah": "keskimääräinen <em>tiivisteiden lasku</em>nopeus ja arvioitu aika valmistumiseen", "ut_etau": "keskimääräinen <em>lataus</em>nopeus ja arvioitu aika valmistumiseen", "ut_etat": "keskimääräinen <em>kokonais</em>nopeus ja arvioitu aika valmistumiseen", "uct_ok": "onnistui", "uct_ng": "ei-hyvä: epäonnistui / hylätty / ei löydy", "uct_done": "ok ja ng yhdistettynä", "uct_bz": "laskee tiivisteitä tai lataa", "uct_q": "tyhjäkäynnillä, odottaa", "utl_name": "tiedostonimi", "utl_ulist": "lista", "utl_ucopy": "kopioi", "utl_links": "linkit", "utl_stat": "tila", "utl_prog": "edistyminen", // keep short: "utl_404": "404", "utl_err": "VIRHE", "utl_oserr": "Käyttöjärjestelmävirhe", "utl_found": "löytyi", "utl_defer": "lykkää", "utl_yolo": "YOLO", "utl_done": "valmis", "ul_flagblk": "tiedostot lisättiin jonoon
          mutta toisen selainvälilehden up2k on kiireinen,
          joten odotetaan sen valmistumista ensin", "ul_btnlk": "palvelinkonfiguraatio on lukinnut tämän kytkimen tähän tilaan", "udt_up": "Lataa", "udt_srch": "Etsi", "udt_drop": "pudota se tähän", "u_nav_m": '
          selvä, mitäs sulla on?
          Enter = Tiedostoja (yksi tai useampi)\nESC = Yksi hakemisto (mukaan lukien alihakemistot)', "u_nav_b": 'TiedostojaYksi hakemisto', "cl_opts": "asetukset", "cl_hfsz": "tiedostokoko", "cl_themes": "teema", "cl_langs": "kieli", "cl_ziptype": "hakemiston pakkaustyyppi", "cl_uopts": "up2k-kytkimet", "cl_favico": "favicon", "cl_bigdir": "suuret hakemistot", "cl_hsort": "#sort", "cl_keytype": "sävellajin notaatiotyyppi", "cl_hiddenc": "piilotetut sarakkeet", "cl_hidec": "piilota", "cl_reset": "palauta", "cl_hpick": "napauta sarakeotsikoita piilottaaksesi alla olevassa taulukossa", "cl_hcancel": "sarakkeiden piilotus peruttu", "cl_rcm": "hiiren pikavalikko", "ct_grid": '田 kuvanäkymä', "ct_ttips": '◔ ◡ ◔">ℹ️ vihjelaatikot', "ct_thumb": 'valitse kuvakkeiden / pienoiskuvien välillä kuvanäkymässä $NPikanäppäin: T">🖼️ pienoiskuvat', "ct_csel": 'käytä CTRL ja SHIFT tiedostojen valintaan kuvanäkymässä">valitse', "ct_dsel": 'käytä aluevalintaa tiedostojen valintaan kuvanäkymässä">aluevalinta', "ct_dl": 'pakota lataus (älä näytä upotettuna), kun tiedostoa klikataan">dl', "ct_ihop": 'kun kuvakatselin suljetaan, vieritä alas viimeksi katsottuun tiedostoon">g⮯', "ct_dots": 'näytä piilotetut tiedostot (jos palvelin sallii)">piilotiedostot', "ct_qdel": 'kysy vahvistusta vain kerran tiedostoja poistaessa">qdel', "ct_dir1st": 'lajittele hakemistot ennen tiedostoja">📁 ensin', "ct_nsort": 'luonnollinen lajittelu (tiedostonimille jotka ovat numeroalkuisia)">nsort', "ct_utc": 'näytä kaikki aikaleimat UTC-ajassa">UTC', "ct_readme": 'näytä README.md hakemistolistauksissa">📜 readme', "ct_idxh": 'näytä index.html hakemistolistan sijasta">htm', "ct_sbars": 'näytä vierityspalkit">⟊', "cut_umod": "jos tiedosto on jo olemassa palvelimella, päivitä palvelimen viimeksi muokattu aikaleima vastaamaan paikallista tiedostoasi (vaatii write- ja delete-oikeudet)\">re📅", "cut_turbo": "yolo-painike -- et todennäköisesti halua ottaa tätä käyttöön:$N$Nkäytä tätä jos latasit valtavan määrän tiedostoja ja jouduit käynnistämään uudelleen jostain syystä, ja haluat jatkaa latausta välittömästi$N$Ntämä korvaa tiivistetarkistuksen yksinkertaisella "onko tällä sama tiedostokoko palvelimella?" joten jos tiedoston sisältö on erilainen sitä EI ladata$N$Nsinun pitäisi poistaa tämä käytöstä kun lataus on valmis, ja sitten "ladata" samat tiedostot uudelleen antaaksesi selaimesi varmistaa ne\">turbo", "cut_datechk": "ei vaikutusta ellei turbo-painike ole käytössä$N$Nvähentää yolo-tekijää hieman; tarkistaa vastaavatko tiedostojen aikaleimat palvelimella omia$N$Npitäisi teoriassa napata useimmat keskeneräiset / vioittuneet lataukset, mutta ei ole korvike varmistuskierrokselle turbo poistettuna käytöstä jälkeenpäin\">päiväysvarmistin", "cut_u2sz": "kunkin lähetyspalan koko (MiB:ssä); suuret arvot lentävät paremmin atlantin yli. kokeile pieniä arvoja erittäin heikoilla yhteyksillä", "cut_flag": "varmista että vain yksi välilehti lataa kerrallaan $N -- muissa välilehdissä täytyy olla tämä käytössä myös $N -- vaikuttaa vain saman verkkotunnuksen välilehtiin", "cut_az": "lähetä tiedostot aakkosjärjestyksessä, eikä pienin-tiedosto-ensiksi$N$Naakkosjärjestys voi tehdä helpommaksi silmäillä jos jokin meni vikaan palvelimella, mutta se tekee latauksesta hieman hitaamman kuitu- ja lähiverkossa", "cut_nag": "käyttöjärjestelmäilmoitus kun lataus valmistuu$N(vain jos selain tai välilehti ei ole aktiivinen)", "cut_sfx": "äänivaroitus kun lataus valmistuu$N(vain jos selain tai välilehti ei ole aktiivinen)", "cut_mt": "monisäikeistä tiedostojen tiivistysarvojen laskeminen$N$Ntämä käyttää web-workereitä ja vaatii$Nenemmän RAM-muistia (jopa 512 MiB ekstraa)$N$Ntekee https:n 30% nopeammaksi, http:n 4.5x nopeammaksi\">mt", "cut_wasm": "käytä wasm:ia selaimen sisäänrakennetun tiivistäjän sijaan; parantaa nopeutta chrome-pohjaisissa selaimissa mutta lisää prosessorikuormaa, ja monissa vanhemmissa chrome-versioissa on bugeja jotka saavat selaimen kuluttamaan kaiken RAM-muistin ja kaatumaan jos tämä on käytössä\">wasm", "cft_text": "favicon-teksti (tyhjennä ja päivitä poistaaksesi käytöstä)", "cft_fg": "edustaväri", "cft_bg": "taustaväri", "cdt_lim": "tiedostojen enimmäismäärä näytettäväksi hakemistossa", "cdt_ask": "sivun lopussa, sen sijaan että lataa $Nautomaattisesti lisää tiedostoja, kysy mitä tehdä", "cdt_hsort": "`kuinka monta lajittelusääntöä (`,sorthref`) sisällyttää media-URL:eihin. Tämän asettaminen nollaan jättää myös huomioimatta media-linkeissä sisällytetyt lajittelusäännöt kun napsautat niitä", "cdt_ren": "ota käyttöön mukautettu pikavalikko, tavallinen valikko on käytettävissä painamalla shift ja napsauttamalla hiiren oikeanpuoleisella painikkeella\">aktivoi", "cdt_rdb": "näytä tavallinen pikavalikko, kun mukautettu on jo auki ja oikeanpuoleista painiketta painetaan uudelleen\">x2", "tt_entree": "näytä navigointipaneeli$NPikanäppäin: B", "tt_detree": "näytä linkkipolku$NPikanäppäin: B", "tt_visdir": "näytä valittu hakemisto", "tt_ftree": "vaihda linkkipolku- / tekstitiedostonäkymään$NPikanäppäin: V", "tt_pdock": "näytä ylähakemistot telakoitussa paneelissa ylhäällä", "tt_dynt": "kasvata automaattisesti hakemistosyvyyden kasvaessa", "tt_wrap": "rivitys", "tt_hover": "paljasta ylivuotavat rivit leijutettaessa$N( rikkoo vierityksen ellei hiiri $N  ole vasemmassa marginaalissa )", "ml_pmode": "hakemiston lopussa...", "ml_btns": "komennot", "ml_tcode": "muunna nämä", "ml_tcode2": "tähän muotoon", "ml_tint": "sävy", "ml_eq": "taajuuskorjain", "ml_drc": "dynaaminen alueen kompressori", "ml_ss": "ohita hiljaiset kohdat", "mt_loop": "toista samaa kappaletta\">🔁", "mt_one": "lopeta yhden toiston jälkeen\">1️⃣", "mt_shuf": "aktivoi satunnaistoisto\">🔀", "mt_aplay": "automaattitoisto jos linkissä jolla pääsit palvelimelle oli kappale-ID$N$Ntämän poistaminen käytöstä pysäyttää myös sivun URL:n päivittämisen kappale-ID:lla musiikkia toistettaessa, estääksesi automaattitoiston jos nämä asetukset menetetään mutta URL säilyy\">a▶", "mt_preload": "aloita seuraavan kappaleen lataaminen lähellä loppua, mahdollistaen saumattoman toiston\">esilataus", "mt_prescan": "siirry seuraavaan hakemistoon ennen viimeisen kappaleen$Nloppumista, pitäen verkkoselaimen tyytyväisenä$Njotta se ei pysäytä toistoa\">nav", "mt_fullpre": "yritä esiladata koko kappale;$N✅ ota käyttöön heikoilla yhteyksillä,$N❌ poista käytöstä hitailla yhteyksillä\">esi+", "mt_fau": "puhelimissa: estä musiikin pysähtyminen jos seuraava kappale ei esilataudu tarpeeksi nopeasti (voi aiheuttaa ongelmia kappaletietojen näyttämisessä)\">☕️", "mt_waves": "aaltomuoto-hakupalkki:$Nnäytä äänenvahvuus selaimessa\">~s", "mt_npclip": "näytä painikkeet parhaillaan soivan kappaleen leikepöydälle kopioimiseen\">/np", "mt_m3u_c": "näytä painikkeet valittujen$Nkappaleiden kopioimiseen m3u8-soittolistana leikepöydälle\">📻", "mt_octl": "käyttöjärjestelmäintegraatio (medianäppäimet / osd)\">os-ctl", "mt_oseek": "salli haku käyttöjärjestelmäintegraation kautta$N$Nhuom: joissakin laitteissa (iPhonet),$Ntämä korvaa 'seuraava kappale' -painikkeen\">kelaus", "mt_oscv": "näytä albumin kansi osd:ssä\">kansikuvat", "mt_follow": "pidä soiva kappale näkyvissä\">🎯", "mt_compact": "kompaktit säätimet\">⟎", "mt_uncache": "tyhjennä välimuisti  (kokeile tätä jos selaimesi välimuistissa on$Nrikkinäinen kopio kappaleesta)\">uncache", "mt_mloop": "toista avoinna olevaa hakemistoa loputtomasti\">🔁 alkuun", "mt_mnext": "lataa seuraava hakemisto ja jatka\">📂 seuraava", "mt_mstop": "pysäytä toisto\">⏸ pysäytä", "mt_cflac": "muunna flac / wav {0}-muotoon\">flac", "mt_caac": "muunna aac / m4a {0}-muotoon\">aac", "mt_coth": "muunna kaikki muut paitsi mp3 {0}-muotoon\">muut", "mt_c2opus": "paras valinta pöytäkoneille, kannettaville, androidille\">opus", "mt_c2owa": "opus-weba, iOS 17.5:lle ja uudemmille\">owa", "mt_c2caf": "opus-caf, iOS 11:lle - 17:lle\">caf", "mt_c2mp3": "käytä tätä erittäin vanhoissa laitteissa\">mp3", "mt_c2flac": "paras äänenlaatu, suuremmat latauskoot\">flac", "mt_c2wav": "pakkaamaton toisto, suurimmat latauskoot\">wav", "mt_c2ok": "hienoa, hyvä valinta", "mt_c2nd": "tuo ei ole suositeltu formaatti laitteellesi, mutta tee miten lystäät", "mt_c2ng": "laitteesi ei näytä tukevan tätä formaattia, mutta yritetään nyt silti", "mt_xowa": "iOS:ssä on bugeja jotka estävät taustatoiston tällä formaatilla; käytä caf:ia tai mp3:a sen sijaan", "mt_tint": "taustan taso (0-100) liukupalkissa$Ntehden puskuroinnista vähemmän häiritsevän", "mt_eq": "`aktivoi taajuuskorjaimen ja vahvistussäätimen;$N$Nvahvistus `0` = normaali 100% äänenvoimakkuus (muokkaamaton)$N$Nleveys `1  ` = normaali stereo (muokkaamaton)$Nleveys `0.5` = 50% vasen-oikea ristisyöttö$Nleveys `0  ` = mono$N$Nvahvistus `-0.8` & leveys `10` = laulun poisto :^)$N$Nequalizerin käyttöönotto tekee saumattomista albumeista täysin saumattomia, joten jätä se päälle kaikilla arvoilla nollassa (paitsi leveys = 1) jos välität siitä", "mt_drc": "aktivoi dynaamisen alueen kompressorin; ottaa myös käyttöön taajuuskorjaimen tasapainottamaan spagettia, joten aseta kaikki EQ-kentät paitsi 'leveys' nollaan jos et halua sitä$N$Nalentaa äänenvoimakkuutta KYNNYS dB:n yläpuolella; jokaisesta SUHDE dB:stä KYNNYKSEN yli tulee 1 dB ulos, joten oletusarvot kynnys -24 ja suhde 12 tarkoittaa ettei sen pitäisi koskaan tulla kovempaa kuin -22 dB ja on turvallista nostaa equalizerin vahvistus 0.8:aan, tai jopa 1.8:aan ATK 0:lla ja valtavalla RLS:llä kuten 90 (toimii vain firefoxissa; RLS on max 1 muissa selaimissa)$N$N(katso wikipedia, he selittävät sen paljon paremmin)", "mt_ss": "`ottaa käyttöön hiljaisuuden ohituksen; moninkertaistaa toistonopeuden `nop`:lla lähellä kappaleen alkua/loppua, kun äänenvoimakkuus on alle `vol` ja kappaleesta on kulunut vasta on `alk`% tai sitä on jäljellä enää `lop`%", "mt_ssvt": "äänenvoimakkuuden kynnysarvo (0-255)\">vol", "mt_ssts": "alkuraja (prosenttia kappaleen alusta)\">alk", "mt_sste": "loppuraja (prosenttia kappaleen lopusta)\">lop", "mt_sssm": "toistonopeuden kerroin (min. 0.15, max. 8))\">nop", "mb_play": "toista", "mm_hashplay": "soita tämä äänitiedosto?", "mm_m3u": "paina Enter/OK Toistaaksesi\npaina ESC/Peruuta Muokataksesi", "mp_breq": "tarvitset firefox 82+ tai chrome 73+ tai iOS 15+", "mm_bload": "ladataan...", "mm_bconv": "muunnetaan muotoon {0}, odota...", "mm_opusen": "selaimesi ei voi toistaa aac / m4a -tiedostoja;\ntranskoodaus opukseen on nyt käytössä", "mm_playerr": "toisto epäonnistui: ", "mm_eabrt": "Toistoyritys peruttiin", "mm_enet": "Internet-yhteytesi on epävakaa", "mm_edec": "Tämä tiedosto on väitetysti vioittunut??", "mm_esupp": "Selaimesi ei ymmärrä tätä äänimuotoa", "mm_eunk": "Tuntematon virhe", "mm_e404": "Kappaletta ei voitu toistaa; virhe 404: Tiedostoa ei löydy.", "mm_e403": "Kappaletta ei voitu toistaa; virhe 403: Pääsy kielletty.\n\nKokeile painaa F5 päivittääksesi, ehkä kirjauduit ulos", "mm_e415": "Kappaletta ei voitu toistaa; virhe 415: Tiedoston muunnos epäonnistui; tarkista palvelimen lokitiedostot.", "mm_e500": "Kappaletta ei voitu toistaa; virhe 500: Tarkista palvelinlokit.", "mm_e5xx": "Kappaletta ei voitu toistaa; palvelinvirhe ", "mm_nof": "ei löydy enempää äänitiedostoja lähistöltä", "mm_prescan": "Etsitään musiikkia toistettavaksi seuraavaksi...", "mm_scank": "Löytyi seuraava kappale:", "mm_uncache": "välimuisti tyhjennetty; kaikki kappaleet ladataan uudelleen seuraavalla toistolla", "mm_hnf": "tuota kappaletta ei enää ole olemassa", "im_hnf": "tuota kuvaa ei enää ole olemassa", "f_empty": 'tämä hakemisto on tyhjä', "f_chide": 'tämä piilottaa sarakkeen «{0}»\n\nvoit palauttaa sarakkeet asetuksista', "f_bigtxt": "tämä tiedosto on {0} Mt kokoinen -- näytetäänkö silti tekstinä?", "f_bigtxt2": "näytetäänkö vain tiedoston loppu? tämä myös mahdollistaa seuraamisen/tailing, näyttäen uudet tekstirivit reaaliaikaisesti", "fbd_more": '
          näytetään {0} / {1} tiedostoa; näytä {2} tai näytä kaikki
          ', "fbd_all": '
          näytetään {0} / {1} tiedostoa; näytä kaikki
          ', "f_anota": "vain {0} / {1} kohdetta valittiin;\nvalitaksesi koko hakemiston, vieritä ensin loppuun", "f_dls": 'nykyisen hakemiston tiedostolinkit on\nvaihdettu latauslinkeiksi', "f_dl_nd": 'ohitetaan kansio (käytä sen sijaan zip/tar-latausta):\n', "f_partial": "Ladataksesi turvallisesti tiedoston joka on parhaillaan latautumassa, klikkaa tiedostoa jolla on sama nimi mutta ilman .PARTIAL päätettä. Paina PERUUTA tai Escape tehdäksesi tämän.\n\nOK / Enter painaminen sivuuttaa tämän varoituksen ja jatkaa .PARTIAL väliaikaistiedoston lataamista, mikä todennäköisesti antaa sinulle vioittunutta dataa.", "ft_paste": "liitä {0} kohdetta$NPikanäppäin: ctrl-V", "fr_eperm": 'ei voida nimetä uudelleen:\nsinulla ei ole “move”-oikeutta tässä hakemistossa', "fd_eperm": 'ei voida poistaa:\nsinulla ei ole “delete” oikeutta tässä hakemistossa', "fc_eperm": 'ei voida leikata:\nsinulla ei ole “move” oikeutta tässä hakemistossa', "fp_eperm": 'ei voida liittää:\nsinulla ei ole “write” oikeutta tässä hakemistossa', "fr_emore": "valitse vähintään yksi kohde uudelleennimettäväksi", "fd_emore": "valitse vähintään yksi kohde poistettavaksi", "fc_emore": "valitse vähintään yksi kohde leikattavaksi", "fcp_emore": "valitse vähintään yksi kohde kopioitavaksi leikepöydälle", "fs_sc": "jaa hakemisto jossa olet", "fs_ss": "jaa valitut tiedostot", "fs_just1d": "et voi valita useampaa kuin yhtä,\ntai sekoittaa tiedostoja ja hakemistoja yhdessä valinnassa", "fs_abrt": "❌ keskeytä", "fs_rand": "🎲 joku.nimi", "fs_go": "✅ luo share", "fs_name": "nimi", "fs_src": "lähde", "fs_pwd": "salasana", "fs_exp": "vanheneminen", "fs_tmin": "min", "fs_thrs": "tuntia", "fs_tdays": "päivää", "fs_never": "ikuinen", "fs_pname": "valinnainen linkin nimi; on satunnainen jos tyhjä", "fs_tsrc": "jaettava tiedosto tai hakemisto", "fs_ppwd": "valinnainen salasana", "fs_w8": "luodaan sharea...", "fs_ok": "paina Enter/OK lisätäksesi leikepöydälle\npaina ESC/Peruuta sulkeaksesi", "frt_dec": "saattaa korjata joitakin rikkinäisiä tiedostonimiä\">url-decode", "frt_rst": "palauta muokatut tiedostonimet takaisin alkuperäisiksi\">↺ palauta", "frt_abrt": "keskeytä ja sulje tämä ikkuna\">❌ peruuta", "frb_apply": "UUDELLEENNIMEÄ", "fr_adv": "erä / liitännäistiedot / kaava uudelleennimeäminen\">lisäasetukset", "fr_case": "isot ja pienet kirjaimet erottava regex\">kirjainkoko", "fr_win": "windows-yhteensopivat nimet; korvaa <>:"\\|?* japanilaisilla leveillä merkeillä\">win", "fr_slash": "korvaa / merkillä joka ei aiheuta uusien hakemistoiden luomista\">ei /", "fr_re": "`regex hakukuvio jota käytetään alkuperäisiin tiedostonimiin; kaappausryhmiin voi viitata alla olevassa muotoilukentässä kuten `(1)` ja `(2)` ja niin edelleen", "fr_fmt": "`foobar2000 innoittama:$N`(title)` korvataan kappaleen nimellä,$N`[(artist) - ](title)` sivuuttaa [tämän] osan jos artisti on tyhjä$N`$lpad((tn),2,0)` ", "fr_pdel": "poista", "fr_pnew": "tallenna nimellä", "fr_pname": "anna nimi uudelle esiasetuksellesi", "fr_aborted": "keskeytetty", "fr_lold": "vanha nimi", "fr_lnew": "uusi nimi", "fr_tags": "valittujen tiedostojen tagit (vain luku, viitetarkoituksiin):", "fr_busy": "nimetään uudelleen {0} kohdetta...\n\n{1}", "fr_efail": "uudelleennimeäminen epäonnistui:\n", "fr_nchg": "{0} uusista nimistä muutettiin win ja/tai ei / vuoksi\n\nJatketaanko näillä muutetuilla uusilla nimillä?", "fd_ok": "poisto OK", "fd_err": "poisto epäonnistui:\n", "fd_none": "mitään ei poistettu; ehkä palvelimen asetukset estivät (xbd)?", "fd_busy": "poistetaan {0} kohdetta...\n\n{1}", "fd_warn1": "POISTA nämä {0} kohdetta?", "fd_warn2": "Viimeinen varoitus! Haluatko varmasti poistaa?", "fc_ok": "siirettiin {0} kohdetta leikepöydälle", "fc_warn": 'siirettiin {0} kohdetta leikepöydälle\n\nmutta: vain tämä selain-välilehti voi liittää ne\n(koska valinta on niin valtavan suuri)', "fcc_ok": "kopioitiin {0} kohdetta leikepöydälle", "fcc_warn": 'kopioitiin {0} kohdetta leikepöydälle\n\nmutta: vain tämä selain-välilehti voi liittää ne\n(koska valinta on niin valtavan suuri)', "fp_apply": "käytä näitä nimiä", "fp_skip": "ohita ristiriitatilanteissa", "fp_ecut": "leikkaa tai kopioi ensin joitakin tiedostoja / hakemistoja liitettäväksi / siirrettäväksi\n\nhuom: voit leikata / liittää eri selain-välilehtien välillä", "fp_ename": "{0} kohdetta ei voida siirtää tänne koska nimet ovat jo käytössä. Anna niille uudet nimet alla jatkaaksesi, tai tyhjennä nimi (\"ohita ristiriitatilanteissa\") ohittaaksesi ne:", "fcp_ename": "{0} kohdetta ei voida kopioida tänne koska nimet ovat jo käytössä. Anna niille uudet nimet alla jatkaaksesi, tai tyhjennä nimi (\"ohita ristiriitatilanteissa\") ohittaaksesi ne:", "fp_emore": "tiedostonimien törmäyksiä on vielä korjaamatta", "fp_ok": "siirto OK", "fcp_ok": "kopiointi OK", "fp_busy": "siirretään {0} kohdetta...\n\n{1}", "fcp_busy": "kopioidaan {0} kohdetta...\n\n{1}", "fp_abrt": "keskeytetään...", "fp_err": "siirto epäonnistui:\n", "fcp_err": "kopiointi epäonnistui:\n", "fp_confirm": "siirrä nämä {0} kohdetta tänne?", "fcp_confirm": "kopioi nämä {0} kohdetta tänne?", "fp_etab": 'leikepöydän lukeminen toisesta selain-välilehdestä epäonnistui', "fp_name": "ladataan tiedostoa laitteeltasi. Anna sille nimi:", "fp_both_m": '
          valitse mitä liittää
          Enter = Siirrä {0} tiedostoa kohteesta «{1}»\nESC = Lataa {2} tiedostoa laitteeltasi', "fcp_both_m": '
          valitse mitä liittää
          Enter = Kopioi {0} tiedostoa kohteesta «{1}»\nESC = Lataa {2} tiedostoa laitteeltasi', "fp_both_b": 'SiirräLähetä', "fcp_both_b": 'KopioiLähetä', "mk_noname": "kirjoita nimi vasemmalla olevaan tekstikenttään ennen kuin teet tuon :p", "nmd_i1": "voit myös lisätä haluamasi tiedostopäätteen, esimerkiksi .md", "nmd_i2": "voit luoda vain .{0}-tiedostoja, koska sinulla ei ole “delete”-oikeutta", "tv_load": "Ladataan tekstidokumenttia:\n\n{0}\n\n{1}% ({2} / {3} Mt ladattu)", "tv_xe1": "tekstitiedoston lataaminen epäonnistui:\n\nvirhe ", "tv_xe2": "404, tiedostoa ei löydy", "tv_lst": "tekstitiedostojen lista hakemistossa", "tvt_close": "palaa hakemistonäkymään$NPikanäppäin: M (tai Esc)\">❌ sulje", "tvt_dl": "lataa tämä tiedosto$NPikanäppäin: Y\">💾 lataa", "tvt_prev": "näytä edellinen dokumentti$NPikanäppäin: i\">⬆ edell", "tvt_next": "näytä seuraava dokumentti$NPikanäppäin: K\">⬇ seur", "tvt_sel": "valitse tiedosto   ( leikkausta / kopiointia / poistoa / ... varten )$NPikanäppäin: S\">val", "tvt_j": "muotoile/siisti json$NPikanäppäin: shift-J\">j", "tvt_edit": "avaa tiedosto tekstieditorissa$NPikanäppäin: E\">✏️ muokkaa", "tvt_tail": "seuraa tiedoston muutoksia; näytä uudet rivit reaaliaikaisesti\">📡 seuraa", "tvt_wrap": "rivitys\">↵", "tvt_atail": "lukitse vieritys sivun alaosaan\">⚓", "tvt_ctail": "dekoodaa terminaalin värit (ansi escape koodit)\">🌈", "tvt_ntail": "vieritysbufferin raja (kuinka monta tavua tekstiä pidetään ladattuna)", "m3u_add1": "kappale lisätty m3u soittolistaan", "m3u_addn": "{0} kappaletta lisätty m3u soittolistaan", "m3u_clip": "m3u soittolista nyt kopioitu leikepöydälle\n\nsinun tulisi luoda uusi tekstitiedosto nimeltä jotain.m3u ja liittää soittolista siihen dokumenttiin; tämä tekee siitä soitettavan", "gt_vau": "älä näytä videoita, toista vain ääni\">🎧", "gt_msel": "aktivoi tiedostonvalintatila; ctrl-klikkaa ohittaaksesi valitsemisen väliaikaisesti$N$N<em>tuplaklikkaa tiedostoa / hakemistoa avataksesi sen</em>$N$NPikanäppäin: S\">valitsin", "gt_crop": "rajaa pienoiskuvat keskeltä\">rajaa", "gt_3x": "korkearesoluutioiset pienoiskuvat\">3x", "gt_zoom": "zoomaa", "gt_chop": "pilko", "gt_sort": "järjestä", "gt_name": "nimi", "gt_sz": "koko", "gt_ts": "päiväys", "gt_ext": "tyyppi", "gt_c1": "rajaa tiedostonimiä enemmän (näytä vähemmän)", "gt_c2": "rajaa tiedostonimiä vähemmän (näytä enemmän)", "sm_w8": "haetaan...", "sm_prev": "alla olevat hakutulokset ovat edellisestä hausta:\n ", "sl_close": "sulje hakutulokset", "sl_hits": "näytetään {0} osumaa", "sl_moar": "lataa lisää", "s_sz": "koko", "s_dt": "päiväys", "s_rd": "polku", "s_fn": "nimi", "s_ta": "tagit", "s_ua": "ylös@", "s_ad": "edist.", "s_s1": "minimi Mt", "s_s2": "maksimi Mt", "s_d1": "min. iso8601", "s_d2": "maks. iso8601", "s_u1": "ladattu jälkeen", "s_u2": "ja/tai ennen", "s_r1": "polku sisältää   (välilyönnillä erotetuttuina)", "s_f1": "nimi sisältää   (negatoi käyttämällä -nope)", "s_t1": "tagit sisältää   (^=alku, loppu=$)", "s_a1": "tietyt metadatan ominaisuudet", "md_eshow": "ei voida renderoida ", "md_off": "[📜readme] poistettu käytöstä [⚙️] -- dokumentti piilotettu", "badreply": "Palvelimen vastauksen jäsentäminen epäonnistui.", "xhr403": "403: Pääsy kielletty\n\nkokeile painaa F5, ehkä sinut kirjattiin ulos", "xhr0": "tuntematon (todennäköisesti yhteys palvelimeen katosi, tai palvelin on pois päältä)", "cf_ok": "sori siitä -- DD" + wah + "oS suojaus aktivoitui\n\nasioiden pitäisi jatkua noin 30 sekunnissa\n\njos mitään ei tapahdu, paina F5 ladataksesi sivun uudelleen", "tl_xe1": "alihakemistojen listaaminen epäonnistui:\n\nvirhe ", "tl_xe2": "404: hakemistoa ei löydy", "fl_xe1": "hakemiston tiedostojen listaaminen epäonnistui:\n\nvirhe ", "fl_xe2": "404: hakemistoa ei löydy", "fd_xe1": "alihakemiston luominen epäonnistui:\n\nvirhe ", "fd_xe2": "404: Ylähakemistoa ei löydy", "fsm_xe1": "viestin lähettäminen epäonnistui:\n\nvirhe ", "fsm_xe2": "404: Ylähakemistoa ei löydy", "fu_xe1": "unpost-listan lataaminen palvelimelta epäonnistui:\n\nvirhe ", "fu_xe2": "404: Tiedostoa ei löydy??", "fz_tar": "pakkaamaton gnu-tar tiedosto (linux / mac)", "fz_pax": "pakkaamaton pax-formaatin tar (hitaampi)", "fz_targz": "gnu-tar gzip tason 3 pakkauksella$N$Nyleensä hyvin hidas, $Nkäytä pakkamatonta tar:ia tämän sijasta", "fz_tarxz": "gnu-tar xz tason 1 pakkauksella$N$Nyleensä hyvin hidas, $Nkäytä pakkamatonta tar:ia tämän sijasta", "fz_zip8": "zip utf8-tiedostonimillä (suattaapi olla epävakaa windows 7:ssa ja vanhemmissa)", "fz_zipd": "zip perinteisillä cp437 tiedostonimillä esihistoriallisille ohjelmistoille", "fz_zipc": "cp437, jossa crc32 laskettu aikaisin,$NMS-DOS PKZIP v2.04g:lle (lokakuu 1993)$N(kestää kauemmin käsitellä ennen latauksen alkua)", "un_m1": "voit poistaa tuoreet tai keskeyttää keskeneräiset latauksesi alta", "un_upd": "päivitä", "un_m4": "tai jakaa alla näkyvät tiedostot:", "un_ulist": "näytä", "un_ucopy": "kopioi", "un_flt": "valinnainen suodatin:  URL:n täytyy sisältää", "un_fclr": "tyhjennä suodatin", "un_derr": 'unpost-poisto epäonnistui:\n', "un_f5": 'jotain hajosi, kokeile päivitystä tai paina F5', "un_uf5": "pahoittelen mutta sinun täytyy päivittää sivu (esimerkiksi painamalla F5 tai CTRL-R) ennen kuin tämä lataus voidaan keskeyttää", "un_nou": 'huom! palvelin liian kiireinen näyttääkseen keskeneräiset lataukset; klikkaa "päivitä" linkkiä hetken kuluttua', "un_noc": 'huom! täysin ladattujen tiedostojen unpost ei ole käytössä/sallittu palvelimen asetuksissa', "un_max": "näytetään ensimmäiset 2000 tiedostoa (käytä suodatinta)", "un_avail": "{0} viimeaikaista latausta voidaan poistaa
          {1} keskeneräistä voidaan keskeyttää", "un_m2": "järjestetty latausajan mukaan; viimeisimmät ensin:", "un_no1": "hupsis! yksikään lataus ei ole riittävän tuore", "un_no2": "hupsis! yksikään tuota suodatinta vastaava lataus ei ole riittävän tuore", "un_next": "poista seuraavat {0} tiedostoa alla", "un_abrt": "keskeytä", "un_del": "poista", "un_m3": "ladataan viimeaikana lähettämiäsi tiedostoja...", "un_busy": "poistetaan {0} tiedostoa...", "un_clip": "{0} linkkiä kopioitu leikepöydälle", "u_https1": "sinun kannattaisi", "u_https2": "vaihtaa https:ään", "u_https3": "paremman suorituskyvyn vuoksi", "u_ancient": 'selaimesi on ns. vaikuttavan ikivanha -- kannattais varmaan käyttää bup:ia tän sijaan', "u_nowork": "tarvitaan firefox 53+ tai chrome 57+ tai iOS 11+", "tail_2old": "tarvitaan firefox 105+ tai chrome 71+ tai iOS 14.5+", "u_nodrop": 'selaimesi on liian vanha vedä-ja-pudota lataamiseen', "u_notdir": "tuo ei ole hakemisto!\n\nselaimesi on liian vanha,\nkokeile sen sijaan 'vedä-pudota'-tekniikkaa.", "u_uri": "'vedä-pudottaaksesi' kuvia muista selainikkunoista,\npudota se isoon latausnapppiin", "u_enpot": 'vaihda peruna UI:hin (voi parantaa latausnopeutta)', "u_depot": 'vaihda ylelliseen UI:hin (voi vähentää latausnopeutta)', "u_gotpot": 'vaihdetaan peruna UI:hin paremman latausnopeuden vuoksi,\n\ntee miten lystäät, jos ei kelpaa!', "u_pott": "

          tiedostot:   {0} valmis,   {1} epäonnistui,   {2} kiireinen,   {3} jonossa

          ", "u_ever": "tämä on peruslatain; up2k tarvitsee vähintään
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'peruslatain; up2k on parempi', "u_uput": 'optimoi latausnopeus (älä laske tarkistussummia)', "u_ewrite": 'sinulla ei ole move-oikeutta tähän hakemistoon', "u_eread": 'sinulla ei ole read-oikeutta tähän hakemistoon', "u_enoi": 'tiedostohaku ei ole käytössä palvelimen asetuksissa', "u_enoow": "ylikirjoitus ei toimi täällä; tarvitaan “Delete”-oikeus", "u_badf": 'Nämä {0} tiedostoa ({1} yhteensä) ohitettiin, mahdollisesti tiedostojärjestelmän oikeuksien vuoksi:\n\n', "u_blankf": 'Nämä {0} tiedostoa ({1} yhteensä) ovat tyhjiä; ladataanko ne silti?\n\n', "u_applef": 'Nämä {0} tiedostoa ({1} yhteensä) ovat todennäköisesti ei-toivottuja;\nPaina OK/Enter OHITTAAKSESI seuraavat tiedostot,\nPaina Peruuta/ESC jos ET halua sulkea pois, ja LATAA nekin:\n\n', "u_just1": '\nEhkä toimii paremmin jos valitset vain yhden tiedoston', "u_ff_many": "jos käytät Linux / MacOS / Android, niin tämä määrä tiedostoja saattaa kaataa Firefoxin!\njos niin käy, kokeile uudelleen (tai käytä Chromea).", "u_up_life": "Tämä lataus poistetaan palvelimelta\n{0} sen valmistumisen jälkeen", "u_asku": 'lataa nämä {0} tiedostoa kohteeseen {1}', "u_unpt": "voit perua / poistaa tämän latauksen käyttämällä vasemmalla ylhäällä olevaa 🧯", "u_bigtab": 'näytetään {0} tiedostoa\n\ntämä voi kaataa selaimesi, oletko varma?', "u_scan": 'Skannataan tiedostoja...', "u_dirstuck": 'hakemistoiteraattori jumittui yrittäessään käyttää seuraavia {0} kohdetta; ohitetaan:', "u_etadone": 'Valmis ({0}, {1} tiedostoa)', "u_etaprep": '(valmistellaan latausta)', "u_hashdone": 'hajautus valmis', "u_hashing": 'hajautus', "u_hs": 'kätellään...', "u_started": "tiedostoja ladataan nyt; tsekkaa [🚀]", "u_dupdefer": "duplikaatti; käsitellään kaikkien muiden tiedostojen jälkeen", "u_actx": "klikkaa tätä tekstiä estääksesi suorituskyvyn
          heikkenemisen vaihtaessasi muihin ikkunoihin/välilehtiin", "u_fixed": "OK!  Hommat hoidossa 👍", "u_cuerr": "chunk {0} / {1} lataus epäonnistui;\ntuskin haittaa, jatketaan\n\ntiedosto: {2}", "u_cuerr2": "palvelin hylkäsi latauksen (chunk {0} / {1});\nyritetään myöhemmin uudelleen\n\ntiedosto: {2}\n\nvirhe ", "u_ehstmp": "yritetään uudelleen; katso oikealta alhaalta", "u_ehsfin": "palvelin hylkäsi pyynnön viimeistellä lataus; yritetään uudelleen...", "u_ehssrch": "palvelin hylkäsi pyynnön suorittaa haku; yritetään uudelleen...", "u_ehsinit": "palvelin hylkäsi pyynnön aloittaa lataus; yritetään uudelleen...", "u_eneths": "verkkovirhe latauksen kättelyssä; yritetään uudelleen...", "u_enethd": "verkkovirhe kohteen olemassaolon testauksessa; yritetään uudelleen...", "u_cbusy": "odotetaan palvelimen luottavan meihin taas verkko-ongelman jälkeen...", "u_ehsdf": "palvelimen levytila loppui!\n\nyritetään jatkuvasti, siinä tapauksessa että joku\nvapauttaa tarpeeksi tilaa jatkamiseen", "u_emtleak1": "näyttää siltä että selaimessasi saattaa olla muistivuoto;\nole hyvä ja", "u_emtleak2": ' vaihda https:ään (suositeltu) tai ', "u_emtleak3": ' ', "u_emtleakc": 'kokeile seuraavaa:\n
          • paina F5 päivittääksesi sivun
          • sitten poista käytöstä  mt  nappi  ⚙️ asetuksissa
          • ja kokeile latausta uudelleen
          Lataukset ovat hieman hitaampia, minkäs teet.\nSori siitä!\n\nPS: chrome v107 sisältää bugfixin tätä varten', "u_emtleakf": 'kokeile seuraavaa:\n
          • paina F5 päivittääksesi sivun
          • sitten ota käyttöön 🥔 (peruna) lataus UI:ssa
          • ja kokeile latausta uudelleen
          \nPS: firefox toivottavasti saa kerättyä itsensä kasaan jossain vaiheessa', "u_s404": "ei löydy palvelimelta", "u_expl": "selitä", "u_maxconn": "useimmat selaimet rajoittavat tämän 6:een, mutta firefox antaa nostaa sitä connections-per-server asetuksella about:config:issa", "u_tu": '

          VAROITUS: turbo päällä,  asiakasohjelma ei välttämättä huomaa jatkaa keskeneräisiä latauksia; katso turbo-napin vihje

          ', "u_ts": '

          VAROITUS: turbo päällä,  hakutulokset voivat olla vääriä; katso turbo-napin vihje

          ', "u_turbo_c": "turbo on poistettu käytöstä palvelimen asetuksissa", "u_turbo_g": "poistetaan turbo käytöstä koska sinulla ei ole\nhakemistolistausoikeuksia tässä asemassa", "u_life_cfg": 'automaattinen poisto min kuluttua (tai tuntia)', "u_life_est": 'lataus poistetaan ---', "u_life_max": 'tämä hakemisto pakottaa\nmaksimi elinajan {0}', "u_unp_ok": 'unpost on sallittu {0}', "u_unp_ng": 'unpost EI ole sallittu', "ue_ro": 'sinulla on vain read-oikeus tähän hakemistoon\n\n', "ue_nl": 'et ole tällä hetkellä kirjautunut sisään', "ue_la": 'olet tällä hetkellä kirjautunut sisään nimellä "{0}"', "ue_sr": 'olet tällä hetkellä tiedostohaku-tilassa\n\nvaihda lataus-tilaan klikkaamalla suurennuslasia 🔎 (suuren HAKU napin vieressä), ja yritä latausta uudelleen\n\npahoittelen', "ue_ta": 'yritä latausta uudelleen, sen pitäisi toimia nyt', "ue_ab": "tätä tiedostoa ladataan jo toiseen hakemistoon, ja se lataus täytyy suorittaa loppuun ennen kuin tiedostoa voidaan ladata muualle.\n\nVoit keskeyttää ja unohtaa alkuperäisen latauksen käyttämällä vasemmalla ylhäällä olevaa 🧯", "ur_1uo": "OK: Tiedosto ladattu onnistuneesti", "ur_auo": "OK: Kaikki {0} tiedostoa ladattu onnistuneesti", "ur_1so": "OK: Tiedosto löytyi palvelimelta", "ur_aso": "OK: Kaikki {0} tiedostoa löytyi palvelimelta", "ur_1un": "Lataus epäonnistui, pahoittelen", "ur_aun": "Kaikki {0} latausta epäonnistui, pahoittelen", "ur_1sn": "Tiedostoa EI löytynyt palvelimelta", "ur_asn": "{0} tiedostoa EI löytynyt palvelimelta", "ur_um": "Valmis;\n{0} latausta OK,\n{1} latausta epäonnistui, pahoittelen", "ur_sm": "Valmis;\n{0} tiedostoa löytyi palvelimelta,\n{1} tiedostoa EI löytynyt palvelimelta", "rc_opn": "avaa", "rc_ply": "toista", "rc_pla": "toista äänitiedostona", "rc_txt": "avaa tekstinäkymässä", "rc_md": "avaa markdown-näkymässä", "rc_dl": "lataa", "rc_zip": "lataa arkistona", "rc_cpl": "kopioi linkki", "rc_del": "poista", "rc_cut": "leikkaa", "rc_cpy": "kopioi", "rc_pst": "liitä", "rc_rnm": "nimeä uudelleen", "rc_nfo": "uusi kansio", "rc_nfi": "uusi tiedosto", "rc_sal": "valitse kaikki", "rc_sin": "käänteinen valinta", "rc_shf": "jaa tämä kansio", "rc_shs": "jaa valinta", "lang_set": "ladataanko sivu uudestaan kielen vaihtamiseksi?", "splash": { "a1": "päivitä", "b1": "hei sie muukalainen   (et ole kirjautunut sisään)", "c1": "kirjaudu ulos", "d1": "tulosta pinojälki", "d2": "näytä kaikkien aktiivisten säikeiden tila", "e1": "päivitä konffit", "e2": "lataa konfiguraatiotiedostot uudelleen (käyttäjätilit/asemat/asemaflagit),$Nja skannaa kaikki e2ds asemat uudelleen$N$Nhuom: kaikki global-asetuksiin$Ntehdyt muutokset vaativat täyden$Nuudelleenkäynnistyksen", "f1": "voit selata näitä:", "g1": "voit ladata näihin:", "cc1": "muuta:", "h1": "poista k304 käytöstä", "i1": "ota k304 käyttöön", "j1": "k304 katkaisee yhteytesi jokaisella HTTP 304:llä, mikä voi estää joitain bugisia välityspalvelimia jumittumasta/lopettamasta sivujen lataamista, mutta se myös vähentää suorituskykyä", "k1": "nollaa asetukset", "l1": "kirjaudu sisään:", "ls3": "kirjaudu sisään", "lu4": "käyttäjätunnus", "lp4": "salasana", "lo3": "kirjaa “{0}” ulos kaikkialta", "lo2": "tämä lopettaa istunnon kaikissa selaimissa", "m1": "tervetuloa takaisin,", "n1": "404: ei löytynyt mitään  ┐( ´ -`)┌", "o1": 'tai ehkä sinulla ei vain ole käyttöoikeuksia? kokeile salasanaa tai mene kotiin', "p1": "403: pääsy kielletty  ~┻━┻", "q1": 'kokeile salasanaa tai mene kotiin', "r1": "mene kotiin", ".s1": "uudelleenkartoita", "t1": "toiminto", "u2": "aika viimeisestä palvelimen kirjoituksesta$N( lataus / uudelleennimeäminen / tms. )$N$N17d = 17 päivää$N1h23 = 1 tunti 23 minuuttia$N4m56 = 4 minuuttia 56 sekuntia", "v1": "yhdistä", "v2": "käytä tätä palvelinta paikallisena kiintolevynä", "w1": "vaihda https:ään", "x1": "vaihda salasana", "y1": "muokkaa jakoja", "z1": "avaa tämä jako:", "ta1": "täytä ensin uusi salasana", "ta2": "toista vahvistaaksesi uuden salasanan:", "ta3": "löytyi kirjoitusvirhe; yritä uudelleen", "nop": "VIRHE: Salasana ei voi olla tyhjä", "nou": "VIRHE: Käyttäjänimi ja/tai salasana ei voi olla tyhjä", "aa1": "saapuvat:", "ab1": "poista no304 käytöstä", "ac1": "ota no304 käyttöön", "ad1": "no304:n lopettaa välimuistin käytön kokonaan; kokeile tätä jos k304 ei riittänyt. Tuhlaa valtavan määrän verkkoliikennettä!", "ae1": "lähtevät:", "af1": "näytä viimeaikaiset lataukset", "ag1": "näytä tunnetut IdP-käyttäjät", } }; ================================================ FILE: copyparty/web/tl/fra.js ================================================ // Les lignes se terminant par //m sont des traductions automatiques non vérifiées Ls.fra = { "tt": "français", "cols": { "c": "bouton d'action", "dur": "durée", "q": "qualité / débit binaire", "Ac": "codec audio", "Vc": "codec vidéo", "Fmt": "format / conteneur", "Ahash": "somme de contrôle audio", "Vhash": "somme de contrôle vidéo", "Res": "résolution", "T": "type de fichier", "aq": "qualité audio / débit binaire", "vq": "qualité vidéo / débit binaire", "pixfmt": "sous-échantillonnage / structure de pixel", "resw": "résolution horizontale", "resh": "résolution verticale", "chs": "canaux audio", "hz": "fréquence", }, "hks": [ [ "misc", ["Échap", "ferme divers menus"], "gestionaire de fichiers", ["G", "activer vue en liste / vue en grille"], ["T", "activer les miniatures / icônes"], ["⇧ A/D", "taille des miniatures"], ["ctrl-K", "suprimer la sélection"], ["ctrl-X", "couper la sélection au presse-papier"], ["ctrl-C", "copier la sélection au presse-papier"], ["ctrl-V", "coller (déplacer/copier) ici"], ["Y", "télécharger la sélection"], ["F2", "renomer la sélection"], "file-list-sel", ["Espace", "activer la sélection de fichiers"], ["↑/↓", "déplacer le selecteur"], ["ctrl ↑/↓", "déplacer le curseur et la zone d'affichage"], ["⇧ ↑/↓", "sélectioner le fichier précédent/suivant"], ["ctrl-A", "sélectionner tout les fichiers / dossiers"], ], [ "navigation", ["B", "basculer la vue en fil d'Ariane / panneau de navigation"], ["I/K", "dossier précédent/suivant"], ["M", "dossier parent (ou réduire le dossier actuel)"], ["V", "activer les dossiers / fichiers texte dans le volet de navigation"], ["A/D", "taille du volet de navigation"], ], [ "lecteur-audio", ["J/L", "chanson précédente/suivante"], ["U/O", "sauter 10s en arrière/avant"], ["0..9", "sauter à 0%..90%"], ["P", "lecture/pause (démarre également la lecture)"], ["S", "sélectionner la chanson en cours"], ["Y", "télécharger le morceau"], ], [ "visionneuse d'image", ["J/L, ←/→", "image précédente/suivante"], ["Début/Fin, ⭦/Fin", "première/dernière image"], ["F", "plein écran"], ["R", "rotation horaire"], ["⇧ R", "rotation antihoraire"], ["S", "sélectionner l'image"], ["Y", "télécharger l'image"], ], [ "lecteur vidéo", ["U/O", "sauter 10s en arrière/avant"], ["P/K/Espace", "lecture/pause"], ["C", "continuer de lire la suivante"], ["V", "lire en boucle"], ["M", "couper le son"], ["[ and ]", "définir l'intervalle de boucle"], ], [ "visionneuse de texte", ["I/K", "fichier précédent/suivant"], ["M", "fermer le fichier texte"], ["E", "modifier le fichier texte"], ["S", "sélectioner le fichier (pour le couper/copier/renommer)"], ["Y", "télécharger le fichier texte"], //m ["⇧ J", "embellir json"], //m ] ], "m_ok": "OK", "m_ng": "Annuler", "enable": "Activer", "danger": "DANGER", "clipped": "copié dans le presse-papier", "ht_s1": "seconde", "ht_s2": "secondes", "ht_m1": "minute", "ht_m2": "minutes", "ht_h1": "heure", "ht_h2": "heures", "ht_d1": "jour", "ht_d2": "jours", "ht_and": " et ", "goh": "panneau-de-commande", "gop": 'élément "frère" précédent">précédent', "gou": 'dossier parent">haut', "gon": 'dossier suivant">suivant', "logout": "Déconnexion ", "login": "Se connecter", //m "access": " accès", "ot_close": "fermer le sous-menu", "ot_search": "`chercher des fichiers par leurs attributs, chemin / nom, tag musicaux, ou nimporte quelle combinaison de ces options$N$N`foo bar` = doit contenir à la fois «foo» et «bar»,$N`foo -bar` = doit contenir «foo» mais pas «bar»,$N`^yana .opus$` = commence par «yana» et est un fichier «opus»$N`"try unite"` = contient exactement «try unite»$N$Nle format de date est iso-8601, comme$N`2009-12-31` ou `2020-09-12 23:30:00`", "ot_unpost": "unpost: supprimer vos téléchargements récents, ou annuler ceux en cours", "ot_bup": "bup: téléverseur de base, prend même en charge netscape 4.0", "ot_mkdir": "mkdir: créer un nouveau répertoire", "ot_md": "new-file: créer un nouveau fichier texte", //m "ot_msg": "msg: envoyer un message au journal du serveur", "ot_mp": "options du lecteur multimedia", "ot_cfg": "options de configuration", "ot_u2i": 'up2k : téléverser des fichiers (si vous avez un accès en écriture) ou basculer en mode recherche pour voir s\'ils existent quelque part sur le serveur$N$Nles téléversements peuvent être repris, ils sont multithreadé, et les horodatages des fichiers sont préservés, mais cela utilise plus de CPU que [🎈]  (le téléverseur de base)

          pendant les téléversements, cette icône devient un indicateur de progression!', "ot_u2w": 'up2k : téléverser des fichiers avec prise en charge de la reprise (fermez votre navigateur et déposez les mêmes fichiers plus tard)$N$multithreadé, et les horodatages des fichiers sont préservés, mais cela utilise plus de CPU que [🎈]  (le téléverseur de base)

          pendant les téléversements, cette icône devient un indicateur de progression!', "ot_noie": 'Utilisez Chrome / Firefox / Edge', "ab_mkdir": "créer un nouveau répertoire", "ab_mkdoc": "nouveau fichier texte", //m "ab_msg": "envoyer un message au journal du serveur", "ay_path": "passer aux dossiers", "ay_files": "passer aux fichiers", "wt_ren": "renommer les éléments sélectionnés$NHotkey: F2", "wt_del": "supprimer les éléments sélectionnés$NHotkey: ctrl-K", "wt_cut": "couper les éléments sélectionnés <small>(puis coller ailleurs)</small>$NHotkey: ctrl-X", "wt_cpy": "copier les éléments sélectionnés dans le presse-papiers$N(pour les coller ailleurs)$NHotkey: ctrl-C", "wt_pst": "coller une sélection précédemment coupée / copiée$NHotkey: ctrl-V", "wt_selall": "sélectionner tous les fichiers$NHotkey: ctrl-A (lorsque le fichier est sélectionné)", "wt_selinv": "inverser la sélection", "wt_zip1": "télécharger ce dossier en tant qu'archive", "wt_selzip": "télécharger la sélection en tant qu'archive", "wt_seldl": "télécharger la sélection en tant que fichiers séparés$NHotkey: Y", "wt_npirc": "copier les informations de la musique au format irc", "wt_nptxt": "copier les informations de la musique en texte brut", "wt_m3ua": "ajouter à la playlist m3u (cliquez sur 📻copier plus tard)", "wt_m3uc": "copier la playlist m3u dans le presse-papiers", "wt_grid": "basculer entre la vue en grille / liste$NHotkey: G", "wt_prev": "musique précédente$NHotkey: J", "wt_play": "lecture / pause$NHotkey: P", "wt_next": "musique suivante$NHotkey: L", "ul_par": "téléversements parallèles:", "ut_rand": "attribution de noms de fichiers aléatoires", "ut_u2ts": "copier l'horodatage de dernière modification$Nde votre système de fichiers vers le serveur\">📅", "ut_ow": "écraser les fichiers existants sur le serveur?$N🛡️: jamais (générera un nouveau nom de fichier à la place)$N🕒: écraser si le fichier sur le serveur est plus ancien que le vôtre$N♻️: toujours écraser si les fichiers sont différents$N⏭️: ignorer systématiquement tous les fichiers existants", //m "ut_mt": "continuer à calculer la somme de contrôle d'autres fichiers pendant le téléversement$N$Npeut-être désactiver si votre CPU ou HDD est la cause de perte de performances", "ut_ask": 'demander confirmation avant le début du téléversement">💭', "ut_pot": "améliorer la vitesse de téléversement sur les appareils lents$Nen simplifiant l'interface utilisateur", "ut_srch": "ne pas réellement téléverser, mais vérifier si les fichiers existent déjà$N sur le serveur (scannera tous les dossiers que vous pouvez lire)", "ut_par": "mettre en pause les téléversements en le réglant sur 0$N$Naugmenter si votre connexion est lente / à forte latence$N$Nle garder à 1 sur le LAN ou si le HDD du serveur est un goulot d'étranglement", "ul_btn": "déposer des fichiers / dossiers
          ici (ou cliquez sur moi)", "ul_btnu": "T É L É V E R S E R", "ul_btns": "C H E R C H E R", "ul_hash": "somme de contrôle", "ul_send": "envoyer", "ul_done": "terminé", "ul_idle1": "aucun téléversement n'est encore dans la file d'attente", "ut_etah": "moyenne <em>hashing</em> vitesse, et temps estimé jusqu'à la fin", "ut_etau": "moyenne <em>upload</em> vitesse et temps estimé jusqu'à la fin", "ut_etat": "moyenne <em>total</em> vitesse et temps estimé jusqu'à la fin", "uct_ok": "terminé avec succès", "uct_ng": "non réussi : échoué / rejeté / non trouvé", "uct_done": "terminés et échoué combinés", "uct_bz": "hachage ou téléversement", "uct_q": "inactif, en attente", "utl_name": "nom de fichier", "utl_ulist": "liste", "utl_ucopy": "copie", "utl_links": "liens", "utl_stat": "état", "utl_prog": "progrès", // keep short: "utl_404": "404", "utl_err": "ERREUR", "utl_oserr": "OS-ERREUR", "utl_found": "trouvé", "utl_defer": "état", "utl_yolo": "YOLO", "utl_done": "terminé", "ul_flagblk": "les fichiers ont été ajoutés à la file d'attente
          cependant, il y a un processus up2k actif dans un autre onglet du navigateur,
          en attente qu'il finisse d'abord", "ul_btnlk": "la configuration du serveur a verrouillé cette options dans cet état", "udt_up": "Téléverser", "udt_srch": "Chercher", "udt_drop": "déposer ici", "u_nav_m": '
          aight, ques-que tu à ?
          Enter = Fichiers (un ou plus)\nESC = Un dossier (sous-dossiers inclus)', "u_nav_b": 'FichiersUn dossier', "cl_opts": "options", "cl_hfsz": "taille du fichier", //m "cl_themes": "thème", "cl_langs": "langue", "cl_ziptype": "téléchargement de dossier", "cl_uopts": "up2k", "cl_favico": "favicon", "cl_bigdir": "gros dossiers", "cl_hsort": "#sort", "cl_keytype": "notation des touches", "cl_hiddenc": "colonnes masquées", "cl_hidec": "masquer", "cl_reset": "réinitialiser", "cl_hpick": "cliquez sur les en-têtes de colonnes pour les masquer dans le tableau ci-dessous", "cl_hcancel": "masquage des colonnes annulé", "cl_rcm": "menu contextuel", //m "ct_grid": '田 grille', "ct_ttips": '◔ ◡ ◔">ℹ️ infobulles', "ct_thumb": 'vue en grille, activer les icônes ou les miniatures$NHotkey: T">🖼️ minia', "ct_csel": 'utiliser CTRL et MAJ pour selectioner des fichiers en vue en grille">sel', "ct_dsel": 'utiliser la sélection par glisser en vue en grille">glisser', //m "ct_dl": 'forcer le téléchargement (ne pas afficher en ligne) lorsqu’un fichier est cliqué">dl', //m "ct_ihop": 'quand le visionneuse d\'image est fermé, faire defiller vers le bas jusqu\'au dernier fichier">g⮯', "ct_dots": 'voir les fichiers caché (si le serveur le permet)">dotfiles', "ct_qdel": 'ne demander qu\'une confirmation lors de la suppression de fichiers>qdel', "ct_dir1st": 'trier les dossiers avant les fichiers">📁 first', "ct_nsort": 'triage par numérotation (pour les nom de fichiers qui sont numérotés)">nsort', "ct_utc": 'voir tout les horodatage en format UTC">UTC', "ct_readme": 'voir le fichier README.md dans le listage des dossiers">📜 readme', "ct_idxh": 'voir une version html (index.html) au-lieu du listage des dossiers normal">htm', "ct_sbars": 'montrer la barre de defilement">⟊', "cut_umod": "si un fichier existe déjà sur le server, mettre à jour l'horodatage de dernière modification du serveur pour qu'il corresponde à votre fichier local (nécessite des autorisations d'écriture et de suppression)\">re📅", "cut_turbo": "le bouton yolo, vous ne voulez probablement PAS activer ceci:$N$Nutilisez ceci si vous téléchargez une grande quantité de fichiers et que vous devez redémarrer pour une raison quelconque, et que vous souhaitez continuer le téléchargement dès que possible$N$Ncela remplace la vérification de hachage par une simple "est-ce que cela a la même taille de fichier sur le serveur?" donc si le contenu du fichier est différent, il ne sera PAS téléchargé$N$Nvous devriez désactiver cela lorsque le téléchargement est terminé, puis "télécharger" les mêmes fichiers à nouveau pour laisser le client les vérifier\">turbo", "cut_datechk": "n'a aucun effet à moins que le bouton turbo ne soit activé$N$Nréduit le facteur yolo d'un tout petit peu ; vérifie si les horodatages des fichiers sur le serveur correspondent aux vôtres$N$Ndevrait théoriquement attraper la plupart des téléchargements inachevés / corrompus, mais n'est pas un substitut à un passage de vérification avec turbo désactivé par la suite\">date-chk", "cut_u2sz": "taille (en MiB) de chaque morceau de téléversement; des grosse valeurs vont mieux passer si la distance entre le serveur et vous est trés grande. Si vous avez une connection trés instable, essayer de plus petites valeurs", "cut_flag": "s'assurer qu'un seul onglet est entrain de mettre un fichier en ligne a la fois $N -- les autres onglets doivent avoir cette option activé aussi $N -- affecte seulement les onglets qui sont sur le même domaine", "cut_az": "mettre en ligne les fichiers dans l'ordre alphabétique, plutôt que le plus petit fichier en premier$N$Nl'ordre alphabétique peut rendre la lecture plus douce sur pour les yeux si quelque chose s'est mal passé sur le serveur, mais cela rend le téléversement légèrement plus lent sur fibre / LAN", "cut_nag": "recevoir une notification via l'OS quand un téléversement finit$N(seulement si le navigateur ou l'onglet n'est pas actif)", "cut_sfx": "alerte audible quand le téléversement finit$N(seulement si le navigateur ou l'onglet n'est pas actif)", "cut_mt": "utiliser le calcul de somme de contrôle multithreadé pour accelerer le processus$N$Ncela utilise des web-workers et nécessite$Nplus de RAM (jusqu'à 512 MiB supplémentaires)$N$NCela rend https 30% plus rapide, http 4.5x plus rapide\">mt", "cut_wasm": "utiliser wasm au lieu du hachage intégré du navigateur; améliore la vitesse sur les navigateurs basés sur chrome mais augmente la charge CPU, et de nombreuses anciennes versions de chrome ont des bugs qui font que le navigateur consomme toute la RAM et plante si cela est activé\">wasm", "cft_text": "text favicon (laisser vide et rafraîchir pour désactiver)", "cft_fg": "couleur de premier plan", "cft_bg": "couleur d'arrière-plan", "cdt_lim": "nombre maximum de fichiers à afficher dans un dossier", "cdt_ask": "lorsque vous faites défiler vers le bas,$Nau lieu de charger plus de fichiers,$Ndemander quoi faire", "cdt_hsort": "`combien de règles de tri (`,sorthref`) à inclure dans les media-URLs. Définir cette valeur à 0 ignorera également les règles de tri incluses dans les liens média lorsque vous cliquez dessus.", "cdt_ren": "activer le menu contextuel personnalisé, le menu normal reste accessible avec shift + clic droit\">activer", //m "cdt_rdb": "afficher le menu clic droit normal lorsque le menu personnalisé est déjà ouvert et qu’on clique à nouveau\">x2", //m "tt_entree": "afficher le panneau de navigation (arborescence des dossiers)$NHotkey: B", "tt_detree": "afficher le fil d’Ariane$NHotkey: B", "tt_visdir": "faire défiler jusqu'au dossier sélectionné", "tt_ftree": "basculer l'arborescence des dossiers / fichiers texte$NHotkey: V", "tt_pdock": "afficher les dossiers parents dans un panneau ancré en haut", "tt_dynt": "croissance automatique à mesure que l'arborescence s'étend", "tt_wrap": "retour à la ligne", "tt_hover": "révéler les lignes débordantes au survol$N( interrompt le défilement à moins que le curseur de la souris ne soit dans la gouttière gauche )", "ml_pmode": "à la fin du dossier…", "ml_btns": "cmds", "ml_tcode": "transcoder", "ml_tcode2": "transcoder vers", "ml_tint": "teinte", "ml_eq": "égaliseur audio", "ml_drc": "compresseur de plage dynamique", "ml_ss": "ignorer les silences", //m "mt_loop": "répéter en boucle une musique\">🔁", "mt_one": "stopper après une musique\">1️⃣", "mt_shuf": "mélanger les musiques dans chaque dossiers\">🔀", "mt_aplay": "jouer automatiquement si le lien utilisé pour accéder au serveur a un song-ID $N$N, désactiver cela arrêtera également la mise à jour de l'URL de la page avec les song-IDs lors de la lecture de la musique, pour éviter la lecture automatique si ces paramètres sont perdus mais que l'URL reste\">a▶", "mt_preload": "commencer à charger la prochaine chanson près de la fin pour une lecture sans interruption\">preload", "mt_prescan": "explorer le dossier suivant avant la dernière musique$Nne finisse, pour garder le navigateur content$Npour qu'il n'arrête pas la lecture\">nav", "mt_fullpre": "essayer de pré-charger la musique entière;$N✅ activer en cas de connection instable,$N❌ désactiver en revanche sur une connection lente va probablement être mieux\">full", "mt_fau": "sur téléphone, empêche la musique de s'arrêter de jouer si la prochaine n'est pas pré-chargée assez rapidement (peut rendre l'affichage des tags buggé)\">☕️", "mt_waves": "barre de progression en spectrograme:$Nmontrer l'amplitude audio dans la miniature\">~s", "mt_npclip": "montrer les boutons pour copier le morceau en cours de lecture\">/np", "mt_m3u_c": "montrer les boutons pour copier les$morceaux sélectionnées en tant qu'entrées de playlist m3u8\">📻", "mt_octl": "intégration os (touches de raccourci multimédia / osd)\">os-ctl", "mt_oseek": "permettre la recherche via l'intégration os$N$Nremarque : sur certains appareils (iPhones),$Ncela remplace le bouton de la chanson suivante\">seek", "mt_oscv": "montrer la couverture de l'album dans l'osd\">art", "mt_follow": "garder la piste en cours défilée dans la vue\">🎯", "mt_compact": "contrôles compacts\">⟎", "mt_uncache": "effacer le cache  (essayez ceci si votre navigateur a mis en cache$Nun copie défectueuse d'une chanson, ce qui empêche sa lecture)\">uncache", "mt_mloop": "lire en boucle le dossier ouvert\">🔁 loop", "mt_mnext": "charger le dossier suivant et continuer\">📂 next", "mt_mstop": "arrêter la lecture\">⏸ stop", "mt_cflac": "convertir flac / wav en {0}\">flac", "mt_caac": "convertir aac / m4a en {0}\">aac", "mt_coth": "convertir tout les autres (pas mp3) en {0}\">oth", "mt_c2opus": "meilleur choix pour PC fixe, PC portable, android\">opus", "mt_c2owa": "opus-weba, pour iOS 17.5 et supérieur\">owa", "mt_c2caf": "opus-caf, pour iOS 11 à 17\">caf", "mt_c2mp3": "utilisez ceci sur des appareils très anciens\">mp3", "mt_c2flac": "meilleure qualité sonore, mais téléchargements énormes\">flac", "mt_c2wav": "lecture non compressée (encore plus gros)\">wav", "mt_c2ok": "bien, bon choix", "mt_c2nd": "ce n'est pas le format de sortie recommandé pour votre appareil, mais ça devrait aller", "mt_c2ng": "votre appareil ne semble pas prendre en charge ce format de sortie, mais essayons quand même", "mt_xowa": "il y a des bugs dans iOS qui empeche d'avoir une lecture en ariere plan en utilisant ce format; utilisez caf ou mp3 à la place", "mt_tint": "niveau d’arrière-plan (0–100) de la barre de progression$Npour rendre la mise en mémoire tampon moins gênante", "mt_eq": "`active l'égaliseur et le contrôle de gain;$N$Nboost `0` = volume standard 100% (non modifié)$N$Nwidth `1  ` = stéréo standard (non modifié)$Nwidth `0.5` = 50% de crossfeed gauche-droite$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = suppression vocale :^)$N$Nl'activation de l'égaliseur rend les albums gapless entièrement gapless, alors laissez-le activé avec toutes les valeurs à zéro (sauf largeur = 1) si vous vous en souciez", "mt_drc": "active le compresseur de plage dynamique (aplanisseur de volume / brickwaller); activera également l'EQ pour équilibrer les choses, donc définissez tous les champs EQ sauf 'width' sur 0 si vous ne le voulez pas$N$Ndiminue le volume de l'audio au-dessus de THRESHOLD dB; pour chaque RATIO dB au-delà de THRESHOLD, il y a 1 dB de sortie, donc des valeurs par défaut de tresh -24 et ratio 12 signifient qu'il ne devrait jamais être plus fort que -22 dB et qu'il est sûr d'augmenter le boost de l'égaliseur à 0.8, ou même 1.8 avec ATK 0 et un énorme RLS comme 90 (ne fonctionne que dans firefox; RLS est max 1 dans les autres navigateurs)$N$N(voir wikipedia, ils expliquent cela beaucoup mieux)", "mt_ss": "`active le saut de silence ; multiplie la vitesse de lecture par `av` près du début/fin quand le volume est sous `vol` et la position est dans les `deb`% premiers ou `fin`% derniers", //m "mt_ssvt": "seuil de volume (0-255)\">vol", //m "mt_ssts": "seuil actif (% piste, début)\">deb", //m "mt_sste": "seuil actif (% piste, fin)\">fin", //m "mt_sssm": "multiplicateur de vitesse de lecture\">av", //m "mb_play": "lecture", "mm_hashplay": "lire ce fichier audio ?", "mm_m3u": "appuyez sur Entrée/OK pour lire\nappuyez sur Échap/Annuler pour modifier", "mp_breq": "nécessite firefox 82+ ou chrome 73+ ou iOS 15+", "mm_bload": "chargement en cours…", "mm_bconv": "conversion en {0}, veuillez patienter…", "mm_opusen": "votre navigateur ne peut pas lire les fichiers aac / m4a ;\nle transcodage en opus est maintenant activé", "mm_playerr": "échec de la lecture : ", "mm_eabrt": "La tentative de lecture a été annulée", "mm_enet": "Votre connexion internet est instable ou inexistante", "mm_edec": "Ce fichier est supposément corrompu??", "mm_esupp": "Votre navigateur ne comprend pas ce format audio", "mm_eunk": "Erreur inconnue", "mm_e404": "Impossible de lire l'audio ; erreur 404 : fichier introuvable.", "mm_e403": "Impossible de lire l'audio ; erreur 403 : accès refusé.\n\nEssayez d'appuyer sur F5 pour recharger, peut-être que vous avez été déconnecté", "mm_e415": "Impossible de lire l'audio ; erreur 415 : échec de la conversion du fichier ; vérifiez les journaux du serveur.", //m "mm_e500": "Impossible de lire l'audio ; erreur 500 : vérifiez les journaux du serveur.", "mm_e5xx": "Impossible de lire l'audio ; erreur serveur ", "mm_nof": "Pas d'autres fichiers audio trouvés par ici", "mm_prescan": "En recherche d'une autre musique à lire…", "mm_scank": "Prochaine musique trouvée :", "mm_uncache": "cache vidé ; toutes les chansons seront retéléchargées lors de la prochaine lecture", "mm_hnf": "cette chanson n'existe plus", "im_hnf": "cette image n'existe plus", "f_empty": 'ce dossier est vide', "f_chide": 'ceci va cacher les colonnes «{0}»\n\ntu peut les réafficher dans les options', "f_bigtxt": "ce fichier fait {0} MiB -- tu veut vraiment le voir en tant que texte ?", "f_bigtxt2": "voir seulement la fin du fichier à la place ? ceci activera aussi le suivi en temps réel, affichant les nouvelles lignes de texte au fur et à mesure", "fbd_more": '
          showing {0} of {1} files; show {2} or show all
          ', "fbd_all": '
          showing {0} of {1} files; show all
          ', "f_anota": "seulement {0} des {1} elements sont selectioné;\npour selectioner le dossier entier, fait défiler jusqu'au fond", "f_dls": 'le lien de fichier dans le répertoire actuel\nà été changé en lien de téléchargement', "f_dl_nd": 'dossier ignoré (utilisez le téléchargement zip/tar à la place):\n', //m "f_partial": "Pour télécharger de façon sécurisée un fichier qui est entrain de se faire téléverser, cliquez sur le fichier qui a le même nom, mais sans l'extension de fichier .PARTIAL. Choisissez ANNULER ou appuiez sur la touche Échap pour faire cela.\n\nAppuyer sur OK / Entrée ignorera cet avertissement et continuera à télécharger le fichier temporaire .PARTIAL à la place, ce qui donnera presque certainement des données corrompues.", "ft_paste": "coller {0} éléments$NHotkey: ctrl-V", "fr_eperm": 'impossible de renommer:\n vous n\'avez pas la permission “move” dans ce dossier', "fd_eperm": 'impossible de supprimer:\nvous n\'avez pas la permission “delete” dans ce dossier', "fc_eperm": 'impossible de couper:\nvous n\'avez pas la permission “move” dans ce dossier', "fp_eperm": 'impossible de coller:\nvous n\'avez pas la permission “write” dans ce dossier', "fr_emore": "sélectionnez au moins un élément à renommer", "fd_emore": "sélectionnez au moins un élément à supprimer", "fc_emore": "sélectionnez au moins un élément à couper", "fcp_emore": "sélectionnez au moins un élément à copier dans le presse-papiers", "fs_sc": "partager le dossier dans lequel vous vous trouvez", "fs_ss": "partager les fichiers sélectionnés", "fs_just1d": "vous ne pouvez pas sélectionner plus d'un dossier,\nou mélanger des fichiers et des dossiers dans une seule sélection", "fs_abrt": "❌ abandonner", "fs_rand": "🎲 nom.aleatoire", "fs_go": "✅ créer partage", "fs_name": "nom", "fs_src": "source", "fs_pwd": "mdp", "fs_exp": "expiration", "fs_tmin": "min", "fs_thrs": "heures", "fs_tdays": "jours", "fs_never": "éternel", "fs_pname": "nom de lien optionnel ; sera aléatoire si vide", "fs_tsrc": "le fichier ou le dossier à partager", "fs_ppwd": "mot de passe optionnel", "fs_w8": "création du partage…", "fs_ok": "appuyez sur Entrée/OK pour le Presse-papiers\nappuyez sur Échap/Annuler pour fermer", "frt_dec": "peut potentiellement réparer certaines instances de noms de fichiers cassés\">url-decode", "frt_rst": "réinitialiser les noms de fichiers modifiés à leurs originaux\">↺ reset", "frt_abrt": "abandonner et fermer cette fenêtre\">❌ cancel", "frb_apply": "APPLIQUER RENOMMER", "fr_adv": "renommage par lot / métadonnées / motif\">advanced", "fr_case": "regex sensible à la casse\">case", "fr_win": "noms windows-safe; remplacer <>:"\\|?* par des caractères japonais en pleine largeur\">win", "fr_slash": "remplacer / par un caractère qui ne provoque pas la création de nouveaux dossiers\">no /", "fr_re": "`modèle de recherche regex à appliquer aux noms de fichiers originaux ; les groupes capturés peuvent être référencés dans le champ de format ci-dessous comme `(1)` et `(2)` et ainsi de suite", "fr_fmt": "`inspiré par foobar2000 : $N`(title)` est remplacé par le titre de la chanson, $N`[(artist) - ](title)` saute [cette] partie si l'artiste est vide, $N`$lpad((tn),2,0)` remplit le numéro de piste à 2 chiffres", "fr_pdel": "supprimer", "fr_pnew": "enregistrer sous", "fr_pname": "donnez un nom pour le nouveau preset", "fr_aborted": "abandonné", "fr_lold": "ancien nom", "fr_lnew": "nouveau nom", "fr_tags": "tags pour les fichier selectioné (lecture-seule, juste pour référence):", "fr_busy": "renomage de {0} items…\n\n{1}", "fr_efail": "renomage a échoué:\n", "fr_nchg": "{0} des nouveaux noms ont été modifiés en raison de win et/ou no /\n\nOK pour continuer avec ces nouveaux noms modifiés ?", "fd_ok": "suppression réussie", "fd_err": "impossible de supprimer:\n", "fd_none": "rien n'a été supprimé ; peut-être bloqué par la configuration du serveur (xbd) ?", "fd_busy": "suppression de {0} éléments…\n\n{1}", "fd_warn1": "SUPPRIMER ces {0} éléments ?", "fd_warn2": "Dernière chance ! Impossible de revenir en arrière. Supprimer ?", "fc_ok": "couper {0} éléments", "fc_warn": 'couper {0} éléments\n\nmais : seul cet onglets peut les coller\n(puisque la sélection est si absolument massive)', "fcc_ok": "copié {0} éléments dans le presse-papiers", "fcc_warn": 'copié {0} éléments dans le presse-papiers\n\nmais : seul cet onglet peut les coller\n(puisque la sélection est si absolument massive)', "fp_apply": "utiliser ces noms", "fp_skip": "ignorer les conflits", //m "fp_ecut": "en premier, coupez ou copiez quelques fichiers / dossiers à coller / déplacer\n\nnote: vous pouvez couper / coller a travers different onglets", "fp_ename": "{0} éléments ne peuvent pas être déplacés ici parce que leurs noms sont déjà pris. Donnez-leur un nouveau nom ci-dessous pour continuer, ou laissez le nom vide (\"ignorer les conflits\") pour les sauter :", //m "fcp_ename": "{0} éléments ne peuvent pas être copiés ici parce que les noms sont déjà pris. Donnez-leur un nouveau nom ci-dessous pour continuer, ou laissez le nom vide (\"ignorer les conflits\") pour les sauter :", //m "fp_emore": "il reste encore des collisions de noms de fichiers à corriger", "fp_ok": "déplacement OK", "fcp_ok": "copie OK", "fp_busy": "déplacement de {0} éléments…\n\n{1}", "fcp_busy": "copie de {0} éléments…\n\n{1}", "fp_abrt": "abandon en cours...", //m "fp_err": "deplacement échoué:\n", "fcp_err": "copie échouée:\n", "fp_confirm": "déplacer ces {0} éléments ici ?", "fcp_confirm": "copier ces {0} éléments ici ?", "fp_etab": 'lecture du presse-papier venant d\'un autre onglet échoué', "fp_name": "téléversement d'un fichier de votre apareil. Donnez lui un nom:", "fp_both_m": '
          choisisez ce qu\'il faut coller
          Entrer = Déplacer {0} fichiers de «{1}»\nESC = Téléverser {2} fichiers de votre appareil', "fcp_both_m": '
          choisissez ce qu\'il faut coller
          Entrer = Copier {0} fichiers de «{1}»\nESC = Téléverser {2} fichiers de votre appareil', "fp_both_b": 'DéplacerTéléverser', "fcp_both_b": 'CopierTéléverser', "mk_noname": "entrez un nom dans le champ de texte à gauche avant de faire ça :p", "nmd_i1": "ajoutez aussi l’extension souhaitée, par exemple .md", //m "nmd_i2": "vous ne pouvez créer que des fichiers .{0} car vous n’avez pas la permission d’effacer", //m "tv_load": "Chargement du document texte:\n\n{0}\n\n{1}% ({2} de {3} MiB chargés)", "tv_xe1": "impossible de charger le fichier texte:\n\nerreur", "tv_xe2": "404, fichier introuvable", "tv_lst": "liste des fichiers texte dans", "tvt_close": "retour a la vue de dossier$NHotkey: M (ou Échap)\">❌ fermer", "tvt_dl": "télécharger ce fichier$NHotkey: Y\">💾 télécharger", "tvt_prev": "montrer le document précédent$NHotkey: i\">⬆ précédent", "tvt_next": "montrer le document suivant$NHotkey: K\">⬇ suivant", "tvt_sel": "sélectionner le fichier   ( pour couper / copier / supprimer / … )$NHotkey: S\">sel", "tvt_j": "embellir json$NHotkey: shift-J\">j", //m "tvt_edit": "ouvrir le fichier dans l'éditeur de texte$NHotkey: E\">✏️ modifier", "tvt_tail": "surveiller le fichier pour les changements; montrer les nouvelles lignes en temps réel\">📡 suivre", "tvt_wrap": "retour à la ligne\">↵", "tvt_atail": "ancrer le défilement au fond de la page\">⚓", "tvt_ctail": "décoder les couleurs du terminal (ansi escape codes)\">🌈", "tvt_ntail": "limite de défilement en arrière (combien d'octets de texte à garder chargé)", "m3u_add1": "musique ajoutée à la playlist m3u", "m3u_addn": "{0} musiques ajoutées à la playlist m3u", "m3u_clip": "la playlist m3u est maintenant copiée dans le presse-papier\n\nvous devriez créer un nouveau fichier texte nommé par exemple playlist.m3u et coller la playlist dans ce fichier ; cela la rendra lisible en tant que playlist", "gt_vau": "ne pas voir les vidéos, juste jouer l'audio\">🎧", "gt_msel": "activer la séléction de fichiers ; ctrl-clic sur un fichier pour override écraser$N$Nquand actif : double-cliquer sur un fichier / dossier pour l'ouvrir$N$NHotkey: S\">multiséléction", "gt_crop": "rogner les miniatures au centre\">rogner", "gt_3x": "miniatures haute résolution\">3x", "gt_zoom": "zoomer", "gt_chop": "rogner", "gt_sort": "trier par", "gt_name": "nom", "gt_sz": "taille", "gt_ts": "date", "gt_ext": "type", "gt_c1": "tronquer les noms de fichiers (montrer moins)", "gt_c2": "tronquer les noms de fichiers (montrer plus)", "sm_w8": "recherche…", "sm_prev": "les résultats de recherche ci-dessous proviennent d'une requête précédente:\n ", "sl_close": "fermer les résultats de recherche", "sl_hits": "affichage de {0} résultats", "sl_moar": "chercher plus", "s_sz": "taille", "s_dt": "date", "s_rd": "chemin", "s_fn": "nom", "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": "téléverser après", "s_u2": "et/ou avant", "s_r1": "le chemin contient   (séparé par des espaces)", "s_f1": "le nom contient   (négation avec -nope)", "s_t1": "les tags contiennent   (^=début, fin=$)", "s_a1": "propriétés de métadonnées spécifiques", "md_eshow": "impossible d'afficher le rendu ", "md_off": "[📜readme] disabled in [⚙️] -- document caché", "badreply": "Échec de l'analyse de la réponse du serveur", "xhr403": "403: Accès refusé\n\nessayez d'appuyer sur F5, peut-être que vous avez été déconnecté", "xhr0": "inconnu (vous avez probablement perdu la connexion au serveur, ou le serveur est hors ligne)", "cf_ok": "désolé pour cela -- la protection DD" + wah + "oS a été déclenché\n\nles choses devraient reprendre dans environ 30 secondes\n\nsi rien ne se passe, appuyez sur F5 pour recharger la page", "tl_xe1": "impossible de lister les sous-dossiers:\n\nerreur ", "tl_xe2": "404: Dossier introuvable", "fl_xe1": "impossible de lister les fichiers dans le dossier:\n\nerreur ", "fl_xe2": "404: Dossier introuvable", "fd_xe1": "impossible de créer le sous-dossier:\n\nerreur ", "fd_xe2": "404: Dossier parent introuvable", "fsm_xe1": "impossible d'envoyer le message:\n\nerreur ", "fsm_xe2": "404: Dossier parent introuvable", "fu_xe1": "échec du chargement de la liste des unpost du serveur:\n\nerreur ", "fu_xe2": "404: Fichier introuvable??", "fz_tar": "fichier gnu-tar non compressé (linux / mac)", "fz_pax": "tar au format pax non compressé (plus lent)", "fz_targz": "gnu-tar avec compression gzip niveau 3$N$Ncela est généralement très lent, donc$Nutilisez plutôt tar non compressé", "fz_tarxz": "gnu-tar avec compression xz niveau 1$N$Ncela est généralement très lent, donc$Nutilisez plutôt tar non compressé", "fz_zip8": "zip avec noms de fichiers utf8 (peut être instable sur windows 7 et versions antérieures)", "fz_zipd": "zip avec noms de fichiers cp437 traditionnels, pour les très anciens logiciels", "fz_zipc": "cp437 avec crc32 calculé tôt,$Nfor MS-DOS PKZIP v2.04g (octobre 1993)$N(prend plus de temps à charger avant que le téléchargement ne commence)", "un_m1": "vous pouvez supprimer vos téléchargements récents (ou annuler ceux en cours) ci-dessous", "un_upd": "rafraîchir", "un_m4": "ou partager les fichiers visibles ci-dessous:", "un_ulist": "montrer", "un_ucopy": "copier", "un_flt": "filtre optionnel:  l'URL doit contenir", "un_fclr": "effacer le filtre", "un_derr": 'échec de l\'unpost-delete:\n', "un_f5": 'quelque chose a cassé, veuillez essayer de rafraîchir ou d\'appuyer sur F5', "un_uf5": "désolé mais vous devez rafraîchir la page (par exemple en appuyant sur F5 ou CTRL-R) avant que ce téléchargement puisse être annulé", "un_nou": 'warning: serveur trop occupé pour afficher les téléversements non finis; cliquez sur le lien "rafraîchir" dans un instant', "un_noc": 'warning: unpost des fichiers entièrement téléchargés n\'est pas activé/permis dans la configuration du serveur', "un_max": "affichage des 2000 premiers fichiers (utilisez le filtre)", "un_avail": "{0} téléchargements récents peuvent être supprimés
          {1} ceux en cours peuvent être annulés", "un_m2": "triés par date de téléchargement; les plus récents en premier:", "un_no1": "sike! aucun téléchargement n'est suffisamment récent", "un_no2": "sike! aucun téléchargement correspondant à ce filtre n'est suffisamment récent", "un_next": "supprimer les {0} fichiers suivants ci-dessous", "un_abrt": "abandonner", "un_del": "supprimer", "un_m3": "chargement de vos téléchargements récents…", "un_busy": "suppression de {0} fichiers…", "un_clip": "{0} liens copiés dans le presse-papiers", "u_https1": "vous devriez", "u_https2": "passer à https", "u_https3": "pour de meilleure performances", "u_ancient": 'votre navigateur est impressionnamment ancien -- vous devriez peut-être utiliser bup à la place', "u_nowork": "nécessite firefox 53+ ou chrome 57+ ou iOS 11+", "tail_2old": "nécessite firefox 105+ ou chrome 71+ ou iOS 14.5+", "u_nodrop": 'votre navigateur est trop ancien pour le téléversement par glisser-déposer', "u_notdir": "ce n'est pas un dossier!\n\nvotre navigateur est trop ancien,\nveuillez essayer le glisser-déposer à la place", "u_uri": "pour glisser-déposer des images depuis d'autres fenêtres de navigateur,\nveuillez les déposer sur le gros bouton de téléversement", "u_enpot": 'passer à l\'interface utilisateur potato (peut améliorer la vitesse de téléversement)', "u_depot": 'passer à l\'interface utilisateur fancy (peut réduire la vitesse de téléversement)', "u_gotpot": 'passage à l\'interface utilisateur potato pour une vitesse de téléversement améliorée,\n\nn\'hésitez pas à revenir en arrière si ça ne vous plaît pas !', "u_pott": "

          fichiers:   {0} fini,   {1} échoué,   {2} en cours,   {3} en attente

          ", "u_ever": "ceci est le téléverseur de base ; up2k nécessite au moins chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'ceci est le téléverseur de base; up2k est meilleur', "u_uput": 'optimiser pour la vitesse (ignorer la somme de contrôle)', "u_ewrite": 'vous n\'avez pas accès en écriture à ce dossier', "u_eread": 'vous n\'avez pas accès en lecture à ce dossier', "u_enoi": 'la recherche de fichiers n\'est pas activée dans la configuration du serveur', "u_enoow": "l'écrasage ne fonctionnera pas ici; besoin de permissions de suppression", "u_badf": 'Ces {0} fichiers (sur {1} au total) ont été ignorés, probablement en raison de permissions système de fichiers:\n\n', "u_blankf": 'Ces {0} fichiers (sur {1} au total) sont vides; les téléverser quand même ?\n\n', "u_applef": 'Ces {0} fichiers (sur {1} au total) sont probablement indésirables;\nAppuyez sur OK/Enter pour IGNORER les fichiers suivants,\nAppuyez sur Annuler/Échap pour NE PAS exclure, et TÉLÉVERSER ceux-ci également:\n\n', "u_just1": '\nPeut-être que cela fonctionne mieux si vous sélectionnez juste un fichier', "u_ff_many": "si vous utilisez Linux / MacOS / Android, alors ce nombre de fichiers peut faire planter Firefox!\nSi cela se produit, veuillez réessayer (ou utiliser Chrome).", "u_up_life": "Ce téléversement va être supprimé du serveur\n{0} après son achèvement", "u_asku": 'téléverser ces {0} fichiers vers {1}', "u_unpt": "vous pouvez défaire / supprimer ce téléversement en utilisant le 🧯 en haut à gauche", "u_bigtab": 'sur le point d\'afficher {0} fichiers\n\ncela peut faire planter votre navigateur, êtes-vous sûr ?', "u_scan": 'Analyse des fichiers…', "u_dirstuck": 'l\'itérateur de répertoire est bloqué en essayant d\'accéder aux {0} éléments suivants ; il sera ignoré :', "u_etadone": 'Terminé ({0}, {1} fichiers)', "u_etaprep": '(préparation au téléversement)', "u_hashdone": 'calcul de la somme de contrôle terminé', "u_hashing": 'calcul de la somme de contrôle', "u_hs": 'établissement d\'une liaison…', "u_started": "les fichiers sont maintenant en cours de téléversement ; voir [🚀]", "u_dupdefer": "dupliqué ; sera traité après tous les autres fichiers", "u_actx": "cliquez sur ce texte pour éviter la perte de
          performance lors du passage à d'autres fenêtres/onglets", "u_fixed": "OK!  Résolu 👍", "u_cuerr": "echec du téléversement du morceau {0} de {1};\nprobablement inoffensif, poursuite\n\nfichier : {2}", "u_cuerr2": "le serveur a rejeté le téléversement (morceau {0} de {1});\nréessaiera plus tard\n\nfichier : {2}\n\nerreur ", "u_ehstmp": "réessaiera ; voir en bas à droite", "u_ehsfin": "le serveur a rejeté la demande de finalisation du téléversement ; nouvelle tentative…", "u_ehssrch": "le serveur a rejeté la demande d'effectuer une recherche ; nouvelle tentative…", "u_ehsinit": "le serveur a rejeté la demande d'initier le téléversement ; nouvelle tentative…", "u_eneths": "erreur réseau lors de l'exécution de l'initialisation du téléversement ; nouvelle tentative…", "u_enethd": "erreur réseau lors du test de l'existence de la cible ; nouvelle tentative…", "u_cbusy": "attente que le serveur nous fasse à nouveau confiance après un problème réseau…", "u_ehsdf": "le serveur est à court d'espace disque !\n\nil va continuer de réessayer, au cas où quelqu'un\nlibérerait suffisamment d'espace pour continuer", "u_emtleak1": "il semble que votre navigateur web ait une fuite de mémoire ;\nveuillez", "u_emtleak2": ' passer à https (recommandé) ou ', "u_emtleak3": ' ', "u_emtleakc": 'essayez la solution suivante:\n
          • appuyez sur F5 pour rafraîchir la page
          • ensuite désactivez le bouton  mt  dans les  ⚙️ paramètres
          • et réessayez ce téléversement
          Les téléversements seront un peu plus lents, mais tant pis.\nDésolé pour le dérangement !\n\nPS : chrome v107 a un correctif pour cela', "u_emtleakf": 'essayez la solution suivante:\n
          • appuyez sur F5 pour rafraîchir la page
          • ensuite activez 🥔 (pomme de terre) dans l\'interface de téléversement
          • et réessayez ce téléversement
          \nPS : firefox aura probablement un correctif à un moment donné', "u_s404": "pas trouvé sur le serveur", "u_expl": "expliquer", "u_maxconn": "la plupart des navigateur limite ceci à 6, mais firefox vous permet de l'augmenter avec connections-per-server dans about:config", "u_tu": '

          WARNING: turbo enclenché,  le client peut ne pas détecter et reprendre les téléversements incomplets ; voir l\'info-bulle du bouton turbo

          ', "u_ts": '

          WARNING: turbo enclenché,  les résultats de recherche peuvent être incorrects ; voir l\'info-bulle du bouton turbo

          ', "u_turbo_c": "turbo est désactivé dans la configuration du serveur", "u_turbo_g": "désactivation de turbo car vous n'avez pas de\nprivilèges de listing de répertoires dans ce volume", "u_life_cfg": 'suppression automatique après min (ou heures)', "u_life_est": 'le téléversement sera supprimé ---', "u_life_max": 'ce dossier impose une\ndurée de vie maximale de {0}', "u_unp_ok": 'unpost est autorisé pour {0}', "u_unp_ng": 'unpost ne sera PAS autorisé', "ue_ro": 'votre accès à ce dossier est en lecture seule\n\n', "ue_nl": 'vous n\'êtes actuellement pas connecté', "ue_la": 'vous êtes actuellement connecté en tant que "{0}"', "ue_sr": 'vous êtes actuellement en mode recherche de fichiers\n\nchangez en mode téléversement en cliquant sur la loupe 🔎 (à côté du grand bouton RECHERCHER), et essayez de téléverser à nouveau\n\ndésolé', "ue_ta": 'essayez de téléverser à nouveau, cela devrait fonctionner maintenant', "ue_ab": "ce fichier a déjà été téléversé dans un autre dossier, et ce téléversement doit être terminé avant que le fichier puisse être téléversé ailleurs.\n\nVous pouvez annuler et oublier le téléversement initial en utilisant le bouton 🧯 en haut à gauche.", "ur_1uo": "OK: Fichier téléversé avec succès", "ur_auo": "OK: Tous les {0} fichiers téléversés avec succès", "ur_1so": "OK: Fichier trouvé sur le serveur", "ur_aso": "OK: Tous les {0} fichiers trouvés sur le serveur", "ur_1un": "Échec du téléversement, désolé", "ur_aun": "Tous les {0} téléversements ont échoué, désolé", "ur_1sn": "Fichier NON trouvé sur le serveur", "ur_asn": "Les {0} fichiers n'ont PAS ÉTÉ trouvés sur le serveur", "ur_um": "Terminé;\n{0} téléversements OK,\n{1} téléversements échoués, désolé", "ur_sm": "Terminé;\n{0} fichiers trouvés sur le serveur,\n{1} fichiers NON trouvés sur le serveur", "rc_opn": "ouvrir", //m "rc_ply": "Lire", //m "rc_pla": "Lire comme audio", //m "rc_txt": "ouvrir dans le visionneur de fichiers", //m "rc_md": "ouvrir dans l’éditeur de texte", //m "rc_dl": "télécharger", //m "rc_zip": "télécharger comme archive", //m "rc_cpl": "copier le lien", //m "rc_del": "supprimer", //m "rc_cut": "couper", //m "rc_cpy": "copier", //m "rc_pst": "coller", //m "rc_rnm": "renommer", //m "rc_nfo": "nouveau dossier", //m "rc_nfi": "nouveau fichier", //m "rc_sal": "tout sélectionner", //m "rc_sin": "inverser la sélection", //m "rc_shf": "partager ce dossier", //m "rc_shs": "partager la sélection", //m "lang_set": "rafraîchir pour que les changements prennent effet ?", "splash": { "a1": "rafraîchir", "b1": "salut étranger   (vous n'êtes pas connecté.)", "c1": "déconnexion", "d1": "vidange de la pile", "d2": "affiche l'état de tous les threads actifs", "e1": "recharger la configuration", "e2": "recharger le fichier de configuration (comptes/volumes/indicateurs de volume),$Net rescanner tous les volumes e2ds$N$Nnote : n'importe quel changement aux paramètres globaux$Nnécessite un redémarrage complet pour prendre effet", "f1": "vous pouvez naviguer :", "g1": "vous pouvez télécharger sur :", "cc1": "autres choses :", "h1": "désactiver k304", "i1": "activer k304", "j1": "activer k304 va déconnecter votre client sur chaque HTTP 304, ce qui peut éviter à certains proxies défectueux de rester bloqués (les pages ne se chargent soudainement plus), mais cela ralentira également les choses en général", "k1": "réinitialiser les paramètres du client", "l1": "connectez-vous pour en savoir plus :", "ls3": "se connecter", //m "lu4": "nom d'utilisateur", //m "lp4": "mot de passe", //m "lo3": "déconnecter “{0}” partout", //m "lo2": "cela mettra fin à la session sur tous les navigateurs", //m "m1": "heureux de vous revoir,", "n1": "404 introuvable  ┐( ´ -`)┌", "o1": 'ou peut-être que vous n\'y avez pas accès -- essayer un mot de passe ou aller à la page d\'accueil', "p1": "403 interdit  ~┻━┻", "q1": 'utiliser un mot de passe ou aller à la page d\'accueil', "r1": "aller à la page d\'accueil", ".s1": "rescanner", "t1": "action", "u2": "temps écoulé depuis la dernière écriture sur le serveur$N(téléchargement/renommage/...)$N$N17j = 17 jours$N1h23 = 1 heure 23 minutes$N4m56 = 4 minutes 56 secondes", "v1": "connecter", "v2": "utilisez ce serveur en tant que disque dur local", "w1": "passer à https", "x1": "changer mot de passe", "y1": "modifier les partages", "z1": "déverrouiller ce partage :", "ta1": "entrez d'abord votre nouveau mot de passe", "ta2": "répétez pour confirmer le nouveau mot de passe :", "ta3": "une faute de frappe a été détectée ; veuillez réessayer.", "nop": "ERREUR : Le mot de passe ne peut pas être vide", //m "nou": "ERREUR : Le nom d’utilisateur et/ou le mot de passe ne peut pas être vide", //m "aa1": "fichiers entrants :", "ab1": "désactiver no304", "ac1": "activer no304", "ad1": "l'activation de no304 désactivera toute mise en cache ; essayez ceci si k304 n'était pas suffisant. Cela va générer un trafic réseau considérable !", "ae1": "téléchargements actifs :", "af1": "afficher les derniers téléchargements", "ag1": "afficher les utilisateurs IdP connus", //m } }; ================================================ FILE: copyparty/web/tl/grc.js ================================================ // Οι γραμμές που τελειώνουν με //m είναι μη επαληθευμένες μηχανικές μεταφράσεις Ls.grc = { "tt": "Ελληνικά", "cols": { "c": "κουμπιά ενεργειών", "dur": "διάρκεια", "q": "ποιότητα / bitrate", "Ac": "κωδικοποιητής ήχου", "Vc": "κωδικοποιητής βίντεο", "Fmt": "μορφή / container", "Ahash": "checksum ήχου", "Vhash": "checksum βίντεο", "Res": "ανάλυση", "T": "τύπος αρχείου", "aq": "ποιότητα ήχου / bitrate", "vq": "ποιότητα βίντεο / bitrate", "pixfmt": "subsampling / δομή εικονοστοιχείων", "resw": "οριζόντια ανάλυση", "resh": "κάθετη ανάλυση", "chs": "κανάλια ήχου", "hz": "συχνότητα δειγματοληψίας", }, "hks": [ [ "διάφορα", ["ESC", "κλείσιμο διαφόρων λειτουργιών"], "διαχειριστής αρχείων", ["G", "εναλλαγή λίστας / πλέγματος"], ["T", "εναλλαγή μικρογραφιών / εικονιδίων"], ["⇧ A/D", "μέγεθος μικρογραφιών"], ["ctrl-K", "διαγραφή επιλεγμένων"], ["ctrl-X", "αποκοπή επιλογής στο πρόχειρο"], ["ctrl-C", "αντιγραφή επιλογής στο πρόχειρο"], ["ctrl-V", "επικόλληση (μετακίνηση/αντιγραφή) εδώ"], ["Y", "λήψη επιλεγμένων"], ["F2", "μετονομασία επιλεγμένων"], "λίστα αρχείων", ["space", "εναλλαγή επιλογής αρχείου"], ["↑/↓", "μετακίνηση δείκτη επιλογής"], ["ctrl ↑/↓", "μετακίνηση δείκτη και προβολής"], ["⇧ ↑/↓", "επιλογή προηγούμενου/επόμενου αρχείου"], ["ctrl-A", "επιλογή όλων των αρχείων / φακέλων"] ], [ "πλοήγηση", ["B", "εναλλαγή σε καρτέλες διαδρομών / δέντρο διαδρομών"], ["I/K", "προηγούμενος/επόμενος φάκελος"], ["M", "γονικός φάκελος (ή σμίκρυνση τρέχοντος)"], ["V", "εναλλαγή φακέλων / δέντρο αρχείων κειμένου"], ["A/D", "μέγεθος πίνακα πλοήγησης"] ], [ "μουσική", ["J/L", "προηγούμενο/επόμενο τραγούδι"], ["U/O", "μετάβαση 10δευτ πίσω/μπροστά"], ["0..9", "μετάβαση στο 0%..90%"], ["P", "αναπαραγωγή/παύση (ξεκινάει κιόλας)"], ["S", "επιλογή αναπαραγόμενου τραγουδιού"], ["Y", "λήψη τραγουδιού"] ], [ "εικόνες", ["J/L, ←/→", "προηγούμενη/επόμενη εικόνα"], ["Home/End", "πρώτη/τελευταία εικόνα"], ["F", "πλήρης οθόνη"], ["R", "περιστροφή δεξιόστροφα"], ["⇧ R", "περιστροφή αριστερόστροφα"], ["S", "επιλογή εικόνας"], ["Y", "λήψη εικόνας"] ], [ "βίντεο", ["U/O", "μετάβαση 10δευτ πίσω/μπροστά"], ["P/K/Space", "αναπαραγωγή/παύση"], ["C", "συνέχεια στο επόμενο"], ["V", "επανάληψη"], ["M", "σίγαση"], ["[ και ]", "ορισμός διαστήματος επανάληψης"] ], [ "αρχεία κειμένου", ["I/K", "προηγούμενο/επόμενο αρχείο"], ["M", "κλείσιμο αρχείου"], ["E", "επεξεργασία αρχείου"], ["S", "επιλογή αρχείου (για αποκοπή/αντιγραφή/μετονομασία)"], ["Y", "λήψη αρχείου κειμένου"], //m ["⇧ J", "ομορφοποίηση json"], //m ] ], "m_ok": "Εντάξει", "m_ng": "Άκυρο", "enable": "Ενεργοποίηση", "danger": "ΚΙΝΔΥΝΟΣ", "clipped": "αντιγράφηκε στο πρόχειρο", "ht_s1": "δευτερόλεπτο", "ht_s2": "δευτερόλεπτα", "ht_m1": "λεπτό", "ht_m2": "λεπτά", "ht_h1": "ώρα", "ht_h2": "ώρες", "ht_d1": "μέρα", "ht_d2": "μέρες", "ht_and": " και ", "goh": "πίνακας ελέγχου", "gop": 'προηγούμενος φάκελος στο ίδιο επίπεδο">προηγούμενο', "gou": 'γονικός φάκελος">πάνω', "gon": 'επόμενος φάκελος">επόμενο', "logout": "Αποσύνδεση ", "login": "Σύνδεση", //m "access": " πρόσβαση", "ot_close": "κλείσιμο υπομενού", "ot_search": "`αναζήτηση αρχείων με βάση χαρακτηριστικά, διαδρομή / όνομα, μουσικά tags ή οποιονδήποτε συνδυασμό$N$N`foo bar` = πρέπει να περιέχει και τα «foo» και «bar»,$N`foo -bar` = πρέπει να περιέχει το «foo» αλλά όχι το «bar»,$N`^yana .opus$` = να ξεκινά με «yana» και να είναι αρχείο «opus»$N`"try unite"` = να περιέχει ακριβώς «try unite»$N$Nη μορφή ημερομηνίας είναι iso-8601, όπως$N`2009-12-31` ή `2020-09-12 23:30:00`", "ot_unpost": "unpost: διαγραφή πρόσφατων μεταφορτώσεων ή ακύρωση ανολοκλήρωτων", "ot_bup": "bup: βασικός uploader, υποστηρίζει μέχρι και netscape 4.0", "ot_mkdir": "mkdir: δημιουργία νέου φακέλου", "ot_md": "new-file: δημιουργία νέου αρχείου κειμένου", //m "ot_msg": "msg: αποστολή μηνύματος στο server log", "ot_mp": "επιλογές media player", "ot_cfg": "επιλογές ρυθμίσεων", "ot_u2i": 'up2k: ανέβασε αρχεία (αν έχεις δικαίωμα εγγραφής) ή ενεργοποίησε τη λειτουργία αναζήτησης για να δεις αν υπάρχουν ήδη στο server$N$Nοι μεταφορτώσεις συνεχίζονται αν διακοπούν, είναι πολυνηματικές και διατηρούν τις χρονοσφραγίδες, αλλά καταναλώνουν περισσότερο CPU από τον [🎈]  (βασικός uploader)

          κατά τη διάρκεια της μεταφόρτωσης, αυτό το εικονίδιο δείχνει την πρόοδό της!', "ot_u2w": 'up2k: ανέβασε αρχεία με υποστήριξη συνέχισης (κλείσε τον browser και ρίξε τα ίδια αρχεία ξανά μετά)$N$Nπολυνηματικό, διατηρεί τις χρονοσφραγίδες, αλλά καταναλώνει περισσότερο CPU από τον [🎈]  (βασικός uploader)

          κατά τη διάρκεια της μεταφόρτωσης, αυτό το εικονίδιο δείχνει την πρόοδό της!', "ot_noie": 'Χρησιμοποίησε Chrome / Firefox / Edge', "ab_mkdir": "δημιουργία φακέλου", "ab_mkdoc": "νέο αρχείο κειμένου", //m "ab_msg": "στείλε μήνυμα στο server log", "ay_path": "πήγαινε σε φακέλους", "ay_files": "πήγαινε σε αρχεία", "wt_ren": "μετονομασία επιλεγμένων$NΣυντόμευση: F2", "wt_del": "διαγραφή επιλεγμένων$NΣυντόμευση: ctrl-K", "wt_cut": "αποκοπή επιλεγμένων <small>(και επικόλληση αλλού)</small>$NΣυντόμευση: ctrl-X", "wt_cpy": "αντιγραφή επιλεγμένων στο πρόχειρο$N(για επικόλληση αλλού)$NΣυντόμευση: ctrl-C", "wt_pst": "επικόλληση αποκομμένων / αντεγραμμένων$NΣυντόμευση: ctrl-V", "wt_selall": "επιλογή όλων$NΣυντόμευση: ctrl-A (με το αρχείο επιλεγμένο)", "wt_selinv": "αντιστροφή επιλογής", "wt_zip1": "κατέβασμα φακέλου ως συμπιεσμένο αρχείο", "wt_selzip": "κατέβασμα επιλογής ως συμπιεσμένο αρχείο", "wt_seldl": "κατέβασμα επιλογής ως μεμονωμένα αρχεία$NΣυντόμευση: Y", "wt_npirc": "αντιγραφή πληροφοριών τραγουδιού σε μορφή irc", "wt_nptxt": "αντιγραφή πληροφοριών τραγουδιού ως κείμενο", "wt_m3ua": "προσθήκη σε m3u λίστα αναπαραγωγής (μετά πάτησε 📻αντιγραφή)", "wt_m3uc": "αντιγραφή m3u λίστας αναπαραγωγής στο πρόχειρο", "wt_grid": "εναλλαγή πλέγματος / λίστας$NΣυντόμευση: G", "wt_prev": "προηγούμενο κομμάτι$NΣυντόμευση: J", "wt_play": "αναπαραγωγή / παύση$NΣυντόμευση: P", "wt_next": "επόμενο κομμάτι$NΣυντόμευση: L", "ul_par": "παράλληλες μεταφορτώσεις:", "ut_rand": "τυχαιοποίηση ονομάτων αρχείων", "ut_u2ts": "αντιγραφή της τελευταίας τροποποιημένης χρονοσφραγίδας αλλαγής$Nαπό το σύστημά σου στον server\">📅", "ut_ow": "αντικατάσταση σε ήδη υπάρχοντα αρχεία του server?$N🛡️: ποτέ (θα δημιουργηθεί νέο όνομα)$N🕒: αν το αρχείο του server είναι παλαιότερο$N♻️: πάντα να αντικαθίστανται αν διαφέρουν$N⏭️: παράλειψη όλων των υπαρχόντων αρχείων χωρίς όρους", //m "ut_mt": "συνέχιση υπολογισμού hash για άλλα αρχεία κατά τη μεταφόρτωση$N$Nαπενεργοποίησέ το αν η CPU ή ο δίσκος σου ζορίζονται", "ut_ask": 'επιβεβαίωση πριν ξεκινήσει η μεταφόρτωση">💭', "ut_pot": "βελτίωση ταχύτητας μεταφόρτωσης σε αργές συσκευές$Nμε απλοποίηση του UI", "ut_srch": "μην ανεβάζεις, έλεγξε αν τα αρχεία$Nυπάρχουν ήδη στον server (ψάχνει σε όλους τους φακέλους που έχεις πρόσβαση)", "ut_par": "κάνε παύση στις μεταφορτώσεις βάζοντάς το 0$N$Nαύξησε το αν έχεις αργή/μεγάλη καθυστέρηση σύνδεσης$N$Nκράτα το 1 σε LAN ή αν ο server έχει αργό δίσκο", "ul_btn": "ρίξε αρχεία / φακέλους
          εδώ (ή κάνε κλικ σε μένα)", "ul_btnu": "Μ Ε Τ Α Φ Ο Ρ Τ Ω Σ Η", "ul_btns": "Α Ν Α Ζ Η Τ Η Σ Η", "ul_hash": "υπολογισμός hash", "ul_send": "αποστολή", "ul_done": "ολοκληρώθηκε", "ul_idle1": "καμία μεταφόρτωση στην ουρά για την ώρα", "ut_etah": "μέση ταχύτητα <em>υπολογισμού hash</em> και εκτίμηση χρόνου μέχρι την ολοκλήρωση", "ut_etau": "μέση ταχύτητα <em>μεταφόρτωσης</em> και εκτίμηση χρόνου μέχρι την ολοκλήρωση", "ut_etat": "μέση <em>συνολική</em> ταχύτητα και εκτίμηση χρόνου μέχρι την ολοκλήρωση", "uct_ok": "ολοκληρώθηκε επιτυχώς", "uct_ng": "no-good: απέτυχε / απορρίφθηκε / δεν βρέθηκε", "uct_done": "ολοκληρωμένα και αποτυχημένα", "uct_bz": "κάνει hash ή μεταφορτώνει", "uct_q": "σε αναμονή, εκκρεμεί", "utl_name": "όνομα αρχείου", "utl_ulist": "λίστα", "utl_ucopy": "αντιγραφή", "utl_links": "σύνδεσμοι", "utl_stat": "κατάσταση", "utl_prog": "πρόοδος", // keep short: "utl_404": "404", "utl_err": "ΣΦΑΛΜΑ", "utl_oserr": "ΣΦ-ΛΕ", "utl_found": "βρέθηκε", "utl_defer": "αναβολή", "utl_yolo": "YOLO", "utl_done": "έγινε", "ul_flagblk": "τα αρχεία προστέθηκαν στην ουρά
          αλλά υπάρχει άλλη ενεργή μεταφόρτωση σε άλλη καρτέλα,
          οπότε περίμενε να τελειώσει αυτό πρώτα", "ul_btnlk": "ο διακομιστής έχει κλειδώσει αυτήν την επιλογή σε αυτήν την κατάσταση", "udt_up": "Μεταφόρτωση", "udt_srch": "Αναζήτηση", "udt_drop": "ρίξ' το εδώ", "u_nav_m": '
          οκ, τι έχουμε εδώ;
          Enter = Αρχεία (ένα ή περισσότερα)\nESC = Ένας φάκελος (μαζί με υποφακέλους)', "u_nav_b": 'ΑρχείαΈνας φάκελος', "cl_opts": "διακόπτες", "cl_hfsz": "μέγεθος αρχείου", //m "cl_themes": "θέμα", "cl_langs": "γλώσσα", "cl_ziptype": "λήψη φακέλου", "cl_uopts": "διακόπτες μεταφόρτωσης", "cl_favico": "favicon", "cl_bigdir": "μεγάλοι φάκελοι", "cl_hsort": "#ταξινόμηση", "cl_keytype": "σημείωση πλήκτρων", "cl_hiddenc": "κρυφές στήλες", "cl_hidec": "κρύψε", "cl_reset": "επανεκκίνηση", "cl_hpick": "πάτησε στις κεφαλίδες στηλών για να τις κρύψεις στον πίνακα παρακάτω", "cl_hcancel": "η απόκρυψη στηλών ακυρώθηκε", "cl_rcm": "μενού δεξιού κλικ", //m "ct_grid": '田 το πλέγμα', "ct_ttips": '◔ ◡ ◔">ℹ️ συμβουλές εργαλείων', "ct_thumb": 'σε προβολή πλέγματος, εναλλαγή εικονιδίων ή μικρογραφιών$NΠλήκτρο συντόμευσης: T">🖼️ μικρογραφίες', "ct_csel": 'χρησιμοποίησε CTRL και SHIFT για επιλογή αρχείων σε προβολή πλέγματος">επιλογή', "ct_dsel": 'χρησιμοποίησε επιλογή με σύρσιμο σε προβολή πλέγματος">σύρσιμο', //m "ct_dl": 'εξαναγκασμός λήψης (να μην εμφανίζεται ενσωματωμένα) όταν γίνεται κλικ σε ένα αρχείο">dl', //m "ct_ihop": 'όταν η προβολή εικόνων κλείνει, κάνε scroll στο τελευταίο προβαλλόμενο αρχείο">g⮯', "ct_dots": 'εμφάνιση κρυφών αρχείων (αν το επιτρέπει ο server)">dotfiles', "ct_qdel": 'όταν διαγράφεις αρχεία, ζήτα επιβεβαίωση μόνο μία φορά">γρήγορη διαγραφή', "ct_dir1st": 'ταξινόμηση φακέλων πριν από τα αρχεία">📁 πρώτα', "ct_nsort": 'φυσική ταξινόμηση (για ονόματα αρχείων με αριθμούς στην αρχή)">φυσική ταξινόμηση', "ct_utc": 'εμφάνιση όλων των ημερομηνιών σε UTC">UTC', "ct_readme": 'εμφάνιση README.md στις λίστες φακέλων">📜 πληροφορίες', "ct_idxh": 'εμφάνιση index.html αντί για λίστα φακέλων">html', "ct_sbars": 'εμφάνιση μπαρών κύλισης">⟊', "cut_umod": "αν το αρχείο υπάρχει ήδη στον server, ενημέρωσέ το με την τελευταία χρονοσφραγίδα τροποποίησης για να ταιριάζει με το τοπικό αρχείο (απαιτεί δικαιώματα εγγραφής+διαγραφής)\">re📅", "cut_turbo": "το κουμπί yolo, πιθανόν να ΜΗΝ ΘΕΛΕΙΣ να το ενεργοποιήσεις:$N$Nχρησιμοποίησέ το αν μεταφορτώνεις πολλά αρχεία και χρειάστηκε να ξαναρχίσεις, και θες να συνεχίσεις τη μεταφότρωση όσο το δυνατόν πιο γρήγορα$N$Nαντικαθιστά τον έλεγχο hash με απλό "έχει το ίδιο μέγεθος αρχείου στον server?" οπότε αν το περιεχόμενο είναι διαφορετικό, ΔΕΝ θα ανέβει$N$Nπρέπει να το κλείσεις όταν τελειώσει η μεταφόρτωση και μετά να "μεταφορτώσεις" πάλι τα ίδια αρχεία για να τα επιβεβαιώσει το τοπικό σου πρόγραμμα\">turbo", "cut_datechk": "δεν επηρεάζει τίποτα εκτός αν το turbo είναι ενεργοποιημένο$N$Nμειώνει λίγο τον παράγοντα yolo; ελέγχει αν οι χρονοσφραγίδες στο διακομιστή ταιριάζουν με τα δικά σου$N$Nπιάνει θεωρητικά τις περισσότερες μισοτελειωμένες/κατεστραμμένες μεταφορτώσεις, αλλά δεν αντικαθιστά τον έλεγχο με το turbo απενεργοποιημένο μετέπειτα\">έλεγχος ημερομηνίας", "cut_u2sz": "μέγεθος (σε MiB) κάθε κομματιού μεταφόρτωσης; μεγάλες τιμές λειτουργούν καλύτερα σε μεγαλύτερες αποστάσεις διακομιστή-πελάτη. Δοκίμασε μικρές τιμές σε πολύ άστατες συνδέσεις", "cut_flag": "εξασφαλίζει ότι μόνο μία καρτέλα μεταφορτώνει κάθε φορά $N -- οι άλλες καρτέλες πρέπει να το έχουν κι αυτές ενεργό $N -- επηρεάζει μόνο τις καρτέλες που βρίσκονται στο ίδιο διεύθυνση", "cut_az": "μεταφόρτωσε τα αρχεία αλφαβητικά, αντί για το μικρότερο αρχείο, πρώτα$N$Nη αλφαβητική σειρά βοηθά να καταλάβεις αν κάτι χάλασε στο διακομιστή αλλά κάνει το ανέβασμα λίγο πιο αργό σε fiber / LAN", "cut_nag": "ειδοποίηση λειτουργικού συστήματος όταν τελειώσει η μεταφόρτωση$N(μόνο αν ο browser ή η καρτέλα δεν είναι ενεργά)", "cut_sfx": "ηχητική ειδοποίηση όταν τελειώσει η μεταφόρτωση$N(μόνο αν ο browser ή η καρτέλα δεν είναι ενεργά)", "cut_mt": "χρησιμοποίησε multithreading για να επιταχύνεις το hashing των αρχείων$N$Nχρησιμοποιεί web-workers και χρειάζεται$Nπερισσότερη RAM (μέχρι 512 MiB επιπλέον)$N$Nκάνει το https 30% πιο γρήγορο, το http 4.5x πιο γρήγορο\">mt", "cut_wasm": "χρησιμοποίησε wasm αντί για τον ενσωματωμένο hasher του browser; βελτιώνει την ταχύτητα σε chrome-based browsers αλλά αυξάνει το φορτίο της CPU, και παλιές εκδόσεις chrome έχουν bugs που κάνουν το browser να τρώει όλη τη RAM και να κρασάρει αν ενεργοποιηθεί\">wasm", "cft_text": "κείμενο favicon (κενό και ανανέωση για απενεργοποίηση)", "cft_fg": "χρώμα προσκηνίου", "cft_bg": "χρώμα παρασκηνίου", "cdt_lim": "μέγιστος αριθμός αρχείων προς εμφάνιση σε ένα φάκελο", "cdt_ask": "όταν φτάνεις στο τέλος,$Nαντί να φορτώσει περισσότερα αρχεία,$Nρωτά τι να κάνει", "cdt_hsort": "`πόσους κανόνες ταξινόμησης (`,sorthref`) να συμπεριλάβει σε URLs πολυμέσων. Αν το βάλεις 0 αγνοεί και κανόνες ταξινόμησης στους συνδέσμους πολυμέσων", "cdt_ren": "ενεργοποίηση προσαρμοσμένου μενού δεξιού κλικ, το κανονικό μενού είναι προσβάσιμο με shift + δεξί κλικ\">ενεργοποίηση", //m "cdt_rdb": "εμφάνιση του κανονικού μενού δεξιού κλικ όταν το προσαρμοσμένο είναι ήδη ανοιχτό και γίνεται ξανά δεξί κλικ\">x2", //m "tt_entree": "εμφάνιση navpane (δέντρο διαδρομών)$NΠλήκτρο συντόμευσης: B", "tt_detree": "εμφάνιση breadcrumbs (καρτέλες διαδρομών)$NΠλήκτρο συντόμευσης: B", "tt_visdir": "κύλιση στον επιλεγμένο φάκελο", "tt_ftree": "εναλλαγή δέντρου διαδρομών / αρχείων κειμένου$NΠλήκτρο συντόμευσης: V", "tt_pdock": "εμφάνιση γονικών φακέλων σε σταθερή μπάρα επάνω", "tt_dynt": "αυτόματη επέκταση καθώς επεκτείνεται το δέντρο διαδρομών", "tt_wrap": "αναδίπλωση λέξεων", "tt_hover": "αποκάλυψη των γραμμών που ξεπερνούν το πλάτος με το ποντίκι πάνω τους$N( σπάει το scroll εκτός αν το ποντίκι $N  είναι στην αριστερή στήλη )", "ml_pmode": "στο τέλος του φακέλου...", "ml_btns": "εντολές", "ml_tcode": "μετακωδικοποίηση", "ml_tcode2": "μετακωδικοποίηση σε", "ml_tint": "φίλτρο χρώματος", "ml_eq": "ισοσταθμιστής ήχου", "ml_drc": "συμπιεστής δυναμικής εμβέλειας", "ml_ss": "παράβλεψη σιωπής", //m "mt_loop": "επανάληψη ενός τραγουδιού\">🔁", "mt_one": "σταμάτα μετά από ένα τραγούδι\">1️⃣", "mt_shuf": "τυχαία σειρά τραγουδιών σε κάθε φάκελο\">🔀", "mt_aplay": "αυτόματη αναπαραγωγή αν υπάρχει song-ID στη διεύθυνση που μπήκες στο διακομιστή$N$Nη απενεργοποίηση αυτού, σταματά το URL από το να ενημερώνεται με τα song-ID ενώ παίζει η μουσική για να αποτραπεί η αυτόματη αναπαραγωγή αν χαθούν αυτές οι ρυθμίσεις αλλά το URL παραμείνει το ίδιο\">a▶", "mt_preload": "ξεκίνα τη φόρτωση του επόμενου τραγουδιού κοντά στο τέλος για συνεχόμενη ακρόαση\">προφόρτωση", "mt_prescan": "πήγαινε στον επόμενο φάκελο πριν τελειώσει το τελευταίο τραγούδι$Nγια να μη σταματήσει το πρόγραμμα περιήγησης να παίζει μουσική\">nav", "mt_fullpre": "προσπάθησε να προφορτώσεις ολόκληρο το τραγούδι;$N✅ ενεργό σε αναξιόπιστες συνδέσεις,$N❌ πιθανότατα απενεργοποιημένο σε αργές συνδέσεις\">πλήρες", "mt_fau": "σε κινητά, πρόλαβε να μην σταματήσει η μουσική αν το επόμενο τραγούδι δεν προφορτώθηκε γρήγορα (μπορεί να προκαλέσει πρόβλημα στην εμφάνιση των ετικετών)\">☕️", "mt_waves": "γραμμή αναζήτησης κυματομορφής:$Nεμφάνιση έντασης ήχου στην μπάρα αναζήτησης\">~s", "mt_npclip": "εμφάνισε κουμπιά για αντιγραφή του τρέχοντος τραγουδιού\">/np", "mt_m3u_c": "εμφάνισε κουμπιά για αντιγραφή των$Nεπιλεγμένων τραγουδιών ως καταχωρήσεις λίστας m3u8\">📻", "mt_octl": "ενσωμάτωση στο λειτουργικό σύστημα (σηντομεύσεις πλήκτρων πολυμέσων / osd)\">έλεγχος-OS", "mt_oseek": "επιτρέπει την αναζήτηση μέσω ενσωμάτωσης του λειτουργικού συστήματος$N$Nσημείωση: σε μερικές συσκευές (iPhones),$Nαντικαθιστά το κουμπί επόμενου τραγουδιού\">αναζήτηση", "mt_oscv": "εμφάνιση εξωφύλλου άλμπουμ σε osd\">εξώφυλλο", "mt_follow": "κρατά το τρέχον κομμάτι ορατό κατά την κύλιση\">🎯", "mt_compact": "συμπαγή κουμπιά ελέγχου\">⟎", "mt_uncache": "καθάρισε την προσωρινή μνήμη  (δοκίμασε αυτό αν ο browser έχει αποθηκεύσει$Nχαλασμένο αντίγραφο τραγουδιού και αρνείται να παίξει)\">εκκαθάριση", "mt_mloop": "τυχαία αναπαραγωγή στον ανοικτό φάκελο\">🔁 τυχαία αναπαραγωγή", "mt_mnext": "φόρτωση επόμενου φακέλου και συνέχιση\">📂 επόμενο", "mt_mstop": "σταμάτησε την αναπαραγωγή\">⏸ σταμάτημα", "mt_cflac": "μετατροπή flac / wav σε {0}\">flac", "mt_caac": "μετατροπή aac / m4a σε {0}\">aac", "mt_coth": "μετατροπή όλων των άλλων (εκτός των mp3) σε {0}\">άλλο", "mt_c2opus": "καλύτερη επιλογή για desktop, laptop, android\">opus", "mt_c2owa": "opus-weba, για iOS 17.5 και νεότερα\">owa", "mt_c2caf": "opus-caf, για iOS 11 έως 17\">caf", "mt_c2mp3": "χρησιμοποίησε αυτό σε πολύ παλιές συσκευές\">mp3", "mt_c2flac": "βέλτιστη ποιότητα ήχου αλλά τεράστιο αρχείο για μεταφόρτωση\">flac", "mt_c2wav": "ασυμπίεστη αναπαραγωγή (ακόμα μεγαλύτερο αρχείο)\">wav", "mt_c2ok": "μια χαρά, σοφή επιλογή", "mt_c2nd": "δεν είναι η προτεινόμενη μορφή εξόδου για τη συσκευή σου, αλλά αυτό είναι ok", "mt_c2ng": "η συσκευή σου φαίνεται να μην υποστηρίζει αυτήν τη μορφή εξόδου, αλλά ας το δοκιμάσουμε ούτως ή άλλως", "mt_xowa": "υπάρχουν bugs σε iOS που εμποδίζουν την αναπαραγωγή στο παρασκήνιο με αυτήν τη μορφή· χρησιμοποίησε caf ή mp3 αντ’ αυτού", "mt_tint": "επίπεδο φόντου (0-100) στην μπάρα αναζήτησης$Nγια να κάνεις το buffering λιγότερο ενοχλητικό", "mt_eq": "`ενεργοποιεί τον ισοσταθμιστή και τον έλεγχο ενίσχυσης;$N$Nενίσχυση `0` = στάνταρ 100% ένταση (απαράλλαχτη)$N$Nεύρος `1  ` = στάνταρ στερεοφωνικό (απαράλλαχτο)$Nεύρος `0.5` = 50% αριστερά-δεξιά μίξη ήχου$Nεύρος `0  ` = μονοφωνικό$N$Nενίσχυση `-0.8` & εύρος `10` = αφαίρεση φωνής :^)$N$Nη ενεργοποίηση του ισοσταθμιστή κάνει τα άλμπουμ χωρίς κενά, να παίζουν χωρίς καθόλου κενά, οπότε άφησέ το ενεργό με όλες τις τιμές στο μηδέν (εκτός από εύρος = 1) αν σε νοιάζει", "mt_drc": "ενεργοποιεί τον συμπιεστή δυναμικής εμβέλειας (εξομάλυνση έντασης / ακραία συμπίεση έντασης); θα ενεργοποιήσει και τον ισοσταθμιστή για να ισορροπήσει τον ήχο, οπότε βάλε όλα τα πεδία ισοσταθμιστή εκτός από το 'εύρος' στο 0 αν δεν το θες$N$Nχαμηλώνει την ένταση του ήχου πάνω από το όριο (THRESHOLD) dB; για κάθε RATIO dB πέρα από το όριο υπάρχει 1 dB εξόδου, οπότε οι προεπιλεγμένες τιμές όριο -24 και 'λόγος' 12 σημαίνουν ότι δεν θα ξεπεράσει ποτέ τα -22 dB και είναι ασφαλές να αυξήσεις την ενίσχυση ισοσταθμιστή σε 0.8, ή και 1.8 με ATK 0 και μεγάλο RLS όπως 90 (δουλεύει μόνο σε firefox· το RLS είναι max 1 σε άλλους browsers)$N$N(δες wikipedia, το εξηγούν καλύτερα)", "mt_ss": "`ἐνεργοποιεῖ τὴν παράλειψιν σιγῆς· πολλαπλασιάζει τὴν ταχύτητα διὰ `ταχ` πλησίον ἀρχῆς/τέλους, ὅταν ἡ φωνὴ ὑπὸ `φων` καὶ ἡ θέσις ἐν τοῖς πρώτοις `ἀρχ`% ἢ τελευταίοις `τελ`%", //m "mt_ssvt": "ὅριον φωνῆς (0-255)\">φων", //m "mt_ssts": "ἐνεργὸν ὅριον (% τροχιᾶς, ἀρχή)\">ἀρχ", //m "mt_sste": "ἐνεργὸν ὅριον (% τροχιᾶς, τέλος)\">τελ", //m "mt_sssm": "πολλαπλασιαστὴς ταχύτητος\">ταχ", //m "mb_play": "παίξε", "mm_hashplay": "να παίξω αυτό το αρχείο ήχου;", "mm_m3u": "πάτα Enter/Εντάξει για Αναπαραγωγή\nπάτα ESC/Άκυρο για Επεξεργασία", "mp_breq": "χρειάζεται firefox 82+ ή chrome 73+ ή iOS 15+", "mm_bload": "φορτώνει...", "mm_bconv": "μετατροπή σε {0}, περίμενε...", "mm_opusen": "ο browser σου δεν παίζει αρχεία aac / m4a;\nη μετατροπή σε opus είναι τώρα ενεργή", "mm_playerr": "η αναπαραγωγή, απέτυχε: ", "mm_eabrt": "Η προσπάθεια αναπαραγωγής ακυρώθηκε", "mm_enet": "Η σύνδεση του ίντερνέτ σου είναι χάλια", "mm_edec": "Το αρχείο αυτό είναι μάλλον κατεστραμμένο;;", "mm_esupp": "Ο browser σου δεν καταλαβαίνει αυτή τη μορφή ήχου", "mm_eunk": "Άγνωστο σφάλμα", "mm_e404": "Αδύνατη η αναπαραγωγή ήχου; σφάλμα 404: Το αρχείο δεν βρέθηκε.", "mm_e403": "Αδύνατη η αναπαραγωγή ήχου; σφάλμα 403: Άρνηση πρόσβασης.\n\nΔοκίμασε F5 για επαναφόρτωση, ίσως να έχεις αποσυνδεθεί", "mm_e415": "Αδύνατη η αναπαραγωγή ήχου; σφάλμα 415: Απέτυχε η μετατροπή αρχείου; έλεγξε τα logs του διακομιστή.", //m "mm_e500": "Αδύνατη η αναπαραγωγή ήχου; σφάλμα 500: Έλεγξε τα logs του διακομιστή.", "mm_e5xx": "Αδύνατη η αναπαραγωγή ήχου; σφάλμα διακομιστή", "mm_nof": "δεν βρέθηκαν άλλα αρχεία ήχου τριγύρω", "mm_prescan": "Αναζήτηση μουσικής για επόμενο τραγούδι...", "mm_scank": "Βρέθηκε το επόμενο τραγούδι:", "mm_uncache": "κρυφή μνήμη καθαρίστηκε· όλα τα τραγούδια θα ξανακατεβούν στην επόμενη αναπαραγωγή", "mm_hnf": "το τραγούδι αυτό πλέον δεν υπάρχει", "im_hnf": "η εικόνα αυτή πλέον δεν υπάρχει", "f_empty": "αυτός ο φάκελος είναι άδειος", "f_chide": "αυτό θα κρύψει τη στήλη «{0}»\n\nμπορείς να εμφανίσεις τις στήλες από τις ρυθμίσεις", "f_bigtxt": "αυτό το αρχείο είναι {0} MiB σε μέγεθος — σίγουρα θέλεις να το δεις ως κείμενο;", "f_bigtxt2": "να δεις μόνο το τέλος του αρχείου αντί για όλο; αυτό ενεργοποιεί και το following/tailing, που δείχνει νέες γραμμές που προστίθενται ζωντανά", "fbd_more": '
          εμφανίζονται {0} από {1} αρχεία; δείξε {2} ή δείξε τα όλα
          ', "fbd_all": '
          εμφανίζονται {0} από {1} αρχεία; δείξε όλα
          ', "f_anota": "μόνο {0} από τα {1} αντικείμενα επιλέχθηκαν;\nγια να επιλέξεις ολόκληρο το φάκελο, κύλησε πρώτα μέχρι κάτω", "f_dls": 'οι σύνδεσμοι αρχείων στον τρέχοντα φάκελο έχουν\nμετατραπεί σε συνδέσμους λήψης', "f_dl_nd": 'παράλειψη φακέλου (χρησιμοποιήστε λήψη zip/tar αντί γι’ αυτό):\n', //m "f_partial": "Για να κατεβάσεις με ασφάλεια ένα αρχείο που ανεβαίνει, κλίκαρε το αρχείο με το ίδιο όνομα, αλλά χωρίς την κατάληξη .PARTIAL. Πάτα Άκυρο ή Escape για να σταματήσεις.\n\nΠάτα Εντάξει / Enter αν αγνοείς την προειδοποίηση και κατέβασε το .PARTIAL αρχείο, που σχεδόν σίγουρα θα είναι κατεστραμμένο.", "ft_paste": "επικόλλησε {0} αντικείμενα$NΠλήκτρο συντόμευσης: ctrl-V", "fr_eperm": 'δεν μπορεί να μετονομαστεί:\nδεν έχεις δικαίωμα “μετακίνησης” σε αυτόν το φάκελο', "fd_eperm": 'δεν μπορεί να διαγραφεί:\nδεν έχεις δικαίωμα “διαγραφής” σε αυτόν το φάκελο', "fc_eperm": 'δεν μπορεί να κοπεί:\nδεν έχεις δικαίωμα “μετακίνησης” σε αυτόν το φάκελο', "fp_eperm": 'δεν μπορεί να επικολληθεί:\nδεν έχεις δικαίωμα “εγγραφής” σε αυτόν το φάκελο', "fr_emore": "επίλεξε τουλάχιστον ένα αντικείμενο για μετονομασία", "fd_emore": "επίλεξε τουλάχιστον ένα αντικείμενο για διαγραφή", "fc_emore": "επίλεξε τουλάχιστον ένα αντικείμενο για αποκοπή", "fcp_emore": "επίλεξε τουλάχιστον ένα αντικείμενο για αντιγραφή στο πρόχειρο", "fs_sc": "μοιράσου το φάκελο που βρίσκεσαι", "fs_ss": "μοιράσου τα επιλεγμένα αρχεία", "fs_just1d": "δεν μπορείς να επιλέξεις περισσότερους από έναν φακέλους,\nή να αναμείξεις αρχεία και φακέλους στην ίδια επιλογή", "fs_abrt": "❌ ακύρωση", "fs_rand": "🎲 τυχαίο όνομα", "fs_go": "✅ δημιούργησε κοινή χρήση", "fs_name": "όνομα", "fs_src": "πηγή", "fs_pwd": "κωδικός", "fs_exp": "λήξη", "fs_tmin": "λεπτά", "fs_thrs": "ώρες", "fs_tdays": "ημέρες", "fs_never": "αιώνιο", "fs_pname": "προαιρετικό όνομα συνδέσμου; αν είναι κενό, θα είναι τυχαίο", "fs_tsrc": "το αρχείο ή ο φάκελος προς κοινή χρήση", "fs_ppwd": "προαιρετικός κωδικός", "fs_w8": "δημιουργία κοινής χρήσης...", "fs_ok": "πάτα Enter/Εντάξει για Πρόχειρο\nπάτα ESC/Άκυρο για Κλείσιμο", "frt_dec": "μπορεί να διορθώσει μερικές περιπτώσεις κατεστραμμένων ονομάτων αρχείων\">αποκωδικοποίηση url", "frt_rst": "επανέφερε τα ονόματα αρχείων στα αρχικά τους\">↺ επαναφορά", "frt_abrt": "ακύρωσε και κλείσε αυτό το παράθυρο\">❌ ακύρωση", "frb_apply": "ΕΦΑΡΜΟΓΗ ΜΕΤΟΝΟΜΑΣΙΑΣ", "fr_adv": "μαζική / μεταδεδομένα / μετονομασία με πρότυπα\">προχωρημένη", "fr_case": "regex με διάκριση πεζών/κεφαλαίων\">case", "fr_win": "ασφαλή ονόματα για windows; αντικαθιστά <>:"\\|?* με ιαπωνικούς χαρακτήρες πλήρους πλάτους\">win", "fr_slash": "αντικαθίσταται / με χαρακτήρα που δεν δημιουργεί νέους φακέλους\">όχι /", "fr_re": "`μοτίβα αναζήτησης (regex) για αναζήτηση στα αρχικά ονόματα; τα καταγραφόμενα groups μπορούν να χρησιμοποιηθούν στο πεδίο μορφοποίησης παρακάτω όπως `(1)` και `(2)` και ούτω καθεξής", "fr_fmt": "`εμπνευσμένο από foobar2000:$N`(title)` αντικαθίσταται από τίτλο τραγουδιού,$N`[(artist) - ](title)` παραλείπει το [this] αν το artist είναι κενό$N`$lpad((tn),2,0)` γεμίζει τον αριθμό κομματιού σε 2 ψηφία", "fr_pdel": "διαγραφή", "fr_pnew": "αποθήκευση ως", "fr_pname": "δώσε όνομα για τη νέα προεπιλογή", "fr_aborted": "ακυρώθηκε", "fr_lold": "παλιό όνομα", "fr_lnew": "νέο όνομα", "fr_tags": "ετικέτες για τα επιλεγμένα αρχεία (μόνο για ανάγνωση):", "fr_busy": "μετονομασία {0} αντικειμένων...\n\n{1}", "fr_efail": "αποτυχία μετονομασίας:\n", "fr_nchg": "{0} από τα νέα ονόματα άλλαξαν λόγω win και/ή όχι /\n\nΕίναι ΟΚ να συνεχίσουμε με αυτά τα ονόματα;", "fd_ok": "διαγραφή OK", "fd_err": "αποτυχία διαγραφής:\n", "fd_none": "δεν διαγράφηκε τίποτα; ίσως μπλοκαρισμένο από τις ρυθμίσεις του διακομιστή (xbd);", "fd_busy": "διαγραφή {0} αντικειμένων...\n\n{1}", "fd_warn1": "ΔΙΑΓΡΑΦΗ αυτών των {0} αντικειμένων;", "fd_warn2": "Τελευταία ευκαιρία! Δεν υπάρχει αναίρεση. Διαγραφή;", "fc_ok": "αποκοπή {0} αντικειμένων", "fc_warn": 'αποκοπή {0} αντικειμένων\n\nαλλά: μόνο αυτή η καρτέλα browser μπορεί να τα επικολλήσει\n(λόγω πολύ μεγάλης επιλογής)', "fcc_ok": "αντιγράφηκαν {0} αντικείμενα στο πρόχειρο", "fcc_warn": "αντιγράφηκαν {0} αντικείμενα στο πρόχειρο\n\nαλλά: μόνο αυτή η καρτέλα browser μπορεί να τα επικολλήσει\n(λόγω πολύ μεγάλης επιλογής)", "fp_apply": "χρησιμοποίησε αυτά τα ονόματα", "fp_skip": "αγνόησε συγκρούσεις", //m "fp_ecut": "πρώτα κάνε αποκοπή ή αντιγραφή κάποιων αρχείων / φακέλων για επικόλληση / μετακίνηση\n\nσημείωση: μπορείς να αποκόπτεις / επικολλάς ανάμεσα σε διαφορετικές καρτέλες browser", "fp_ename": "τα {0} αντικείμενα δεν μπορούν να μετακινηθούν εδώ γιατί τα ονόματα υπάρχουν ήδη. Δώσε νέα ονόματα παρακάτω για να συνεχίσεις, ή άφησε κενό (\"αγνόησε συγκρούσεις\") για να τα αγνοήσεις:", //m "fcp_ename": "τα {0} αντικείμενα δεν μπορούν να αντιγραφούν εδώ γιατί τα ονόματα υπάρχουν ήδη. Δώσε νέα ονόματα παρακάτω για να συνεχίσεις, ή άφησε κενό (\"αγνόησε συγκρούσεις\") για να τα αγνοήσεις:", //m "fp_emore": "υπάρχουν ακόμα συγκρούσεις ονομάτων που πρέπει να διορθωθούν", "fp_ok": "μετακίνηση OK", "fcp_ok": "αντιγραφή OK", "fp_busy": "μετακίνηση {0} αντικειμένων...\n\n{1}", "fcp_busy": "αντιγραφή {0} αντικειμένων...\n\n{1}", "fp_abrt": "γίνεται ακύρωση...", //m "fp_err": "αποτυχία μετακίνησης:\n", "fcp_err": "αποτυχία αντιγραφής:\n", "fp_confirm": "να μετακινηθούν αυτά τα {0} αντικείμενα εδώ;", "fcp_confirm": "να αντιγραφούν αυτά τα {0} αντικείμενα εδώ;", "fp_etab": "αποτυχία ανάγνωσης πρόχειρου από άλλη καρτέλα browser", "fp_name": "μεταφόρτωση αρχείου από τη συσκευή σου. Δώσε του όνομα:", "fp_both_m": '
          διάλεξε τι θα επικολλήσεις
          Enter = Μετακίνηση {0} αρχείων από «{1}»\nESC = Μεταφόρτωση {2} αρχείων από τη συσκευή σου', "fcp_both_m": '
          διάλεξε τι θα επικολλήσεις
          Enter = Αντιγραφή {0} αρχείων από «{1}»\nESC = Μεταφόρτωση {2} αρχείων από τη συσκευή σου', "fp_both_b": 'ΜετακίνησηΜεταφόρτωση', "fcp_both_b": 'ΑντιγραφήΜεταφόρτωση', "mk_noname": "γράψε ένα όνομα στο πεδίο κειμένου αριστερά πριν το κάνεις :p", "nmd_i1": "μπορείτε επίσης να προσθέσετε την κατάληξη που θέλετε, όπως .md", //m "nmd_i2": "μπορείτε να δημιουργήσετε μόνο αρχεία .{0} επειδή δεν έχετε δικαίωμα διαγραφής", //m "tv_load": "Φόρτωση αρχείου κειμένου:\n\n{0}\n\n{1}% ({2} από {3} MiB φορτωμένα)", "tv_xe1": "αδυναμία φόρτωσης αρχείου κειμένου:\n\nσφάλμα ", "tv_xe2": "404, αρχείο δεν βρέθηκε", "tv_lst": "λίστα αρχείων κειμένου σε", "tvt_close": "επιστροφή στην προβολή φακέλου$NΣυντόμευση: M (ή Esc)\">❌ κλείσιμο", "tvt_dl": "κατέβασε αυτό το αρχείο$NΣυντόμευση: Y\">💾 λήψη", "tvt_prev": "προβολή προηγούμενου εγγράφου$NΣυντόμευση: i\">⬆ προηγούμενο", "tvt_next": "προβολή επόμενου εγγράφου$NΣυντόμευση: K\">⬇ επόμενο", "tvt_sel": "επέλεξε αρχείο   (για αποκοπή / αντιγραφή / διαγραφή / ...)$NΣυντόμευση: S\">επιλογή", "tvt_j": "ομορφοποίηση json$NΣυντόμευση: shift-J\">j", //m "tvt_edit": "άνοιγμα αρχείου στον επεξεργαστή κειμένου$NΣυντόμευση: E\">✏️ επεξεργασία", "tvt_tail": "παρακολούθηση αρχείου για αλλαγές; εμφάνιση νέων γραμμών σε πραγματικό χρόνο\">📡 παρακολούθηση", "tvt_wrap": "αναδίπλωση λέξεων\">↵", "tvt_atail": "κλείδωμα κύλισης στο κάτω μέρος\">⚓", "tvt_ctail": "αποκωδικοποίηση χρωμάτων τερματικού (ansi escape codes)\">🌈", "tvt_ntail": "όριο κύλισης (πόσα bytes κειμένου να κρατούνται φορτωμένα)", "m3u_add1": "το τραγούδι προστέθηκε στη λίστα m3u", "m3u_addn": "προστέθηκαν {0} τραγούδια στη λίστα m3u", "m3u_clip": "η λίστα m3u αντιγράφηκε στο πρόχειρο\n\nπρέπει να φτιάξεις ένα νέο αρχείο κειμένου με όνομα η_λίστα_μου.m3u και να επικολλήσεις τη λίστα μέσα· αυτό θα το καταστήσει αναπαράξιμο", "gt_vau": "μην δείχνεις το βίντεο, παίξε μόνο τον ήχο\">🎧", "gt_msel": "ενεργοποίηση επιλογής αρχείων; ctrl-κλικ σε αρχείο για παράκαμψη$N$N<em>όταν είναι ενεργό: διπλό κλικ σε αρχείο / φάκελο το ανοίγει</em>$N$NΣυντόμευση: S\">πολλαπλή επιλογή", "gt_crop": "κεντραρισμένη περικοπή μικρογραφιών\">περικοπή", "gt_3x": "μικρογραφίες υψηλής ανάλυσης\">3x", "gt_zoom": "ζουμ", "gt_chop": "κόψε", "gt_sort": "ταξινόμηση κατά", "gt_name": "όνομα", "gt_sz": "μέγεθος", "gt_ts": "ημερομηνία", "gt_ext": "τύπος", "gt_c1": "μεγαλύτερη περικοπή ονομάτων αρχείων (δείξε λιγότερα)", "gt_c2": "μικρότερη περικοπή ονομάτων αρχείων (δείξε περισσότερα)", "sm_w8": "αναζήτηση...", "sm_prev": "τα παρακάτω αποτελέσματα αναζήτησης προέρχονται από προηγούμενη αναζήτηση:\n ", "sl_close": "κλείσιμο αποτελεσμάτων αναζήτησης", "sl_hits": "εμφανίζονται {0} αποτελέσματα", "sl_moar": "φόρτωσε περισσότερα", "s_sz": "μέγεθος", "s_dt": "ημερομηνία", "s_rd": "μονοπάτι", "s_fn": "όνομα", "s_ta": "ετικέτες", "s_ua": "ανέβηκε@", "s_ad": "προχωρ.", "s_s1": "ελάχιστο σε MiB", "s_s2": "μέγιστο σε MiB", "s_d1": "ελάχιστο iso8601", "s_d2": "μέγιστο iso8601", "s_u1": "μεταφορτώθηκε αργότερα", "s_u2": "και/ή πριν", "s_r1": "το μονοπάτι περιέχει   (χωρισμένα με κενό)", "s_f1": "το όνομα περιέχει   (άρνηση με -nope)", "s_t1": "οι ετικέτες περιέχουν   (^=αρχή, τέλος=$)", "s_a1": "συγκεκριμένες ιδιότητες μεταδεδομένων", "md_eshow": "δεν μπορεί να εμφανιστεί ", "md_off": "[📜readme] απενεργοποιημένο στο [⚙️] -- κρυμμένο έγγραφο", "badreply": "Αποτυχία ανάλυσης απάντησης από το διακομιστή", "xhr403": "403: Πρόσβαση αρνήθηκε\n\nδοκίμασε το F5, ίσως αποσυνδέθηκες", "xhr0": "άγνωστο (πιθανόν αποσύνδεση από το διακομιστή ή ο διακομιστής είναι εκτός σύνδεσης)", "cf_ok": "συγγνώμη γι' αυτό -- η προστασία DD" + wah + "oS ενεργοποιήθηκε\n\nοι διαδικασίες θα συνεχιστούν σε περίπου 30 δευτερόλεπτα\n\nαν δεν γίνει τίποτα, πάτα F5 για επαναφόρτωση", "tl_xe1": "αδύνατη η λίστα υποφακέλων:\n\nσφάλμα ", "tl_xe2": "404: Ο φάκελος δεν βρέθηκε", "fl_xe1": "αδύνατη η λίστα αρχείων σε φάκελο:\n\nσφάλμα ", "fl_xe2": "404: Ο φάκελος δεν βρέθηκε", "fd_xe1": "αδύνατη η δημιουργία υποφακέλου:\n\nσφάλμα ", "fd_xe2": "404: Ο γονικός φάκελος δεν βρέθηκε", "fsm_xe1": "αδύνατη η αποστολή μηνύματος:\n\nσφάλμα ", "fsm_xe2": "404: Ο γονικός φάκελος δεν βρέθηκε", "fu_xe1": "αποτυχία φόρτωσης λίστας unpost από το διακομιστή:\n\nσφάλμα ", "fu_xe2": "404: Το αρχείο δεν βρέθηκε??", "fz_tar": "μη συμπιεσμένο αρχείο gnu-tar (linux / mac)", "fz_pax": "μη συμπιεσμένο pax-format tar (πιο αργό)", "fz_targz": "gnu-tar με συμπίεση gzip επίπεδο 3$N$Nσυνήθως πολύ αργό, οπότε$Nχρησιμοποίησε καλύτερα μη συμπιεσμένο tar", "fz_tarxz": "gnu-tar με συμπίεση xz επίπεδο 1$N$Nσυνήθως πολύ αργό, οπότε$Nχρησιμοποίησε καλύτερα μη συμπιεσμένο tar", "fz_zip8": "zip με ονόματα αρχείων utf8 (ίσως να κολλάει σε windows 7 και παλιότερα)", "fz_zipd": "zip με παραδοσιακά ονόματα cp437, για πολύ παλιό λογισμικό", "fz_zipc": "cp437 με crc32 υπολογισμένο νωρίτερα,$Nγια MS-DOS PKZIP v2.04g (οκτώβριος 1993)$N(παίρνει παραπάνω χρόνο πριν ξεκινήσει η μεταφόρτωση)", "un_m1": "μπορείς να διαγράψεις τα πρόσφατα αρχεία που μεταφόρτωσες (ή να ακυρώσεις τα μισοτελειωμένα) παρακάτω", "un_upd": "ανανέωση", "un_m4": "ή μοιράσου τα αρχεία που βλέπεις παρακάτω:", "un_ulist": "εμφάνιση", "un_ucopy": "αντιγραφή", "un_flt": "προαιρετικό φίλτρο:  η διεύθυνση πρέπει να περιέχει", "un_fclr": "καθαρισμός φίλτρου", "un_derr": "αποτυχία διαγραφής unpost:\n", "un_f5": "κάτι χάλασε, δοκίμασε την ανανέωση ή πάτα F5", "un_uf5": "συγγνώμη αλλά πρέπει να ανανεώσεις τη σελίδα (πχ με F5 ή CTRL-R) πριν ακυρώσεις αυτήν την αποστολή", "un_nou": "προσοχή: ο διακομιστής είναι πολύ φορτωμένος για να δείξει μισοτελειωμένες αποστολές· πάτα την ανανέωση, σε λίγο", "un_noc": "προσοχή: το unpost των ολοκληρωμένων αρχείων δεν επιτρέπεται από τη ρύθμιση του διακομιστή", "un_max": "εμφανίζονται τα πρώτα 2000 αρχεία (χρησιμοποίησε φίλτρο)", "un_avail": "μπορείς να διαγράψεις {0} πρόσφατα αρχεία
          μπορείς να ακυρώσεις {1} μισοτελειωμένες αποστολές", "un_m2": "ταξινομημένα κατά χρόνο μεταφόρτωσης; τα πιο πρόσφατα πρώτα:", "un_no1": "άκυρο! καμία μεταφόρτωση δεν είναι αρκετά πρόσφατη", "un_no2": "άκυρο! καμία μεταφόρτωση με αυτό το φίλτρο δεν είναι αρκετά πρόσφατη", "un_next": "διάγραψε τα επόμενα {0} αρχεία παρακάτω", "un_abrt": "άκυρο", "un_del": "διαγραφή", "un_m3": "φορτώνω τις πρόσφατες μεταφορτώσεις σου...", "un_busy": "διαγράφω {0} αρχεία...", "un_clip": "αντιγράφηκαν {0} σύνδεσμοι στο πρόχειρο", "u_https1": "πρέπει", "u_https2": "μετάβαση σε https", "u_https3": "για καλύτερη απόδοση", "u_ancient": 'ο browser σου είναι εντυπωσιακά απαρχαιωμένος — ίσως να χρησιμοποιήσεις το bup αντί γι\' αυτό', "u_nowork": "χρειάζεται firefox 53+ ή chrome 57+ ή iOS 11+", "tail_2old": "χρειάζεται firefox 105+ ή chrome 71+ ή iOS 14.5+", "u_nodrop": "ο browser σου είναι πολύ παλιός για drag&drop μεταφορτώσεις", "u_notdir": "αυτός δεν είναι φάκελος!\n\nο browser σου είναι πολύ παλιός,\nδοκίμασε drag&drop αντ' αυτού", "u_uri": "για να κάνεις drag&drop εικόνων από άλλα παράθυρα browser,\nρίξ' τες πάνω στο μεγάλο κουμπί μεταφόρτωσης", "u_enpot": 'άλλαξε στο potato UI (ίσως ανεβάζει πιο γρήγορα)', "u_depot": 'άλλαξε στο fancy UI (ίσως ανεβάζει πιο αργά)', "u_gotpot": "αλλάζω στο potato UI για πιο γρήγορη μεταφόρτωση,\n\nμπορείς να το αλλάξεις πάλι αν θες!", "u_pott": "

          αρχεία:   {0} ολοκληρωμένα,   {1} αποτυχημένα,   {2} σε εξέλιξη,   {3} σε ουρά

          ", "u_ever": "αυτός είναι ο βασικός uploader; το up2k θέλει τουλάχιστον
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'αυτός είναι ο βασικός uploader; το up2k είναι καλύτερο', "u_uput": "βελτιστοποίηση για ταχύτητα (παράλειψη ελέγχου ακεραιότητας)", "u_ewrite": "δεν έχεις δικαίωμα εγγραφής σε αυτόν τον φάκελο", "u_eread": "δεν έχεις δικαίωμα ανάγνωσης σε αυτόν τον φάκελο", "u_enoi": "η αναζήτηση αρχείων δεν είναι ενεργοποιημένη στο αρχείο ρυθμίσεων του διακομιστή", "u_enoow": "δεν μπορείς να κάνεις αντικατάσταση εδώ· χρειάζεται δικαίωμα Διαγραφής", "u_badf": "Αυτά τα {0} αρχεία (από {1} συνολικά) παραλείφθηκαν, πιθανώς λόγω δικαιωμάτων συστήματος αρχείων:\n\n", "u_blankf": "Αυτά τα {0} αρχεία (από {1} συνολικά) είναι άδεια / κενά· να τα μεταφορτώσω έτσι κι αλλιώς;\n\n", "u_applef": "Αυτά τα {0} αρχεία (από {1} συνολικά) πιθανώς δεν είναι επιθυμητά;\nΠάτα Εντάξει/Enter για ΝΑ ΑΓΝΟΗΘΟΥΝ τα παρακάτω αρχεία,\nΠάτα Άκυρο/ESC για ΝΑ ΜΗΝ ΑΠΟΚΛΕΙΣΤΟΥΝ και να ΜΕΤΑΦΟΡΤΩΘΟΎΝ κι αυτά:\n\n", "u_just1": "\nΊσως δουλέψει καλύτερα αν επιλέξεις μόνο ένα αρχείο", "u_ff_many": "αν χρησιμοποιείς Linux / MacOS / Android, τότε τόσα αρχεία μπορεί να κατάρρευση του Firefox!\nαν γίνει αυτό, δοκίμασε ξανά (ή χρησιμοποίησε τον Chrome).", "u_up_life": "Αυτή η μεταφόρτωση θα διαγραφεί από το διακομιστή\n{0} μετά την ολοκλήρωσή της", "u_asku": "μεταφόρτωση αυτών των {0} αρχείων στο {1}", "u_unpt": "μπορείς να αναιρέσεις / διαγράψεις αυτήν τη μεταφόρτωση χρησιμοποιώντας το 🧯, πάνω αριστερά", "u_bigtab": "θα εμφανιστούν {0} αρχεία\n\nαυτό μπορεί να κάνει τον browser σου να κολλήσει, είσαι σίγουρος;", "u_scan": "Σάρωση αρχείων...", "u_dirstuck": "ο επεξεργαστής φακέλων κόλλησε προσπαθώντας να προσπελάσει τα εξής {0} αντικείμενα· θα τα παραλείψει:", "u_etadone": "Ολοκληρώθηκε ({0}, {1} αρχεία)", "u_etaprep": "(προετοιμασία για μεταφόρτωση)", "u_hashdone": "το hashing ολοκληρώθηκε", "u_hashing": "υπολογισμός hash", "u_hs": "handshaking...", "u_started": "τα αρχεία ανεβαίνουν τώρα· δες τα στο [🚀]", "u_dupdefer": "διπλότυπο; θα επεξεργαστεί μετά από όλα τα άλλα αρχεία", "u_actx": "πάτα αυτό το κείμενο για να μην χάσεις
          απόδοση όταν αλλάζεις παράθυρα/καρτέλες", "u_fixed": "ΟΚ!  Το διόρθωσα 👍", "u_cuerr": "αποτυχία μεταφόρτωσης τμήματοςς {0} από {1};\nπιθανώς ακίνδυνο, συνεχίζω\n\nαρχείο: {2}", "u_cuerr2": "ο διακομιστής απέρριψε τη μεταφόρτωση (τμήμα {0} από {1});\nθα ξαναδοκιμάσει αργότερα\n\nαρχείο: {2}\n\nσφάλμα ", "u_ehstmp": "θα ξαναδοκιμάσει; δες κάτω δεξιά", "u_ehsfin": "ο διακομιστής απέρριψε το αίτημα ολοκλήρωσης της μεταφόρτωσης; ξαναδοκιμάζει...", "u_ehssrch": "ο διακομιστής απέρριψε το αίτημα αναζήτησης; ξαναδοκιμάζει...", "u_ehsinit": "ο διακομιστής απέρριψε το αίτημα για εκκίνηση μεταφόρτωσης; ξαναδοκιμάζει...", "u_eneths": "σφάλμα δικτύου κατά το handshake μεταφόρτωσης; ξαναδοκιμάζει...", "u_enethd": "σφάλμα δικτύου κατά τον έλεγχο ύπαρξης στόχου; ξαναδοκιμάζει...", "u_cbusy": "ο διακομιστής περιμένει να μας εμπιστευτεί ξανά μετά από πρόβλημα δικτύου...", "u_ehsdf": "ο διακομιστής έμεινε από χώρο στο δίσκο!\n\nθα συνεχίσει να ξαναδοκιμάζει,\nσε περίπτωση που κάποιος\nελευθερώσει αρκετό χώρο για συνέχεια", "u_emtleak1": "φαίνεται πως ο browser σου έχει διαρροή μνήμης;\nπαρακαλώ", "u_emtleak2": ' αλλαγή σε https (συνιστάται) ή ', "u_emtleak3": ' ', "u_emtleakc": 'δοκίμασε τα εξής:\n
          • πάτα F5 για ανανέωση σελίδας
          • μετά απενεργοποίησε το  mt  κουμπί στις  ⚙️ ρυθμίσεις
          • και δοκίμασε ξανά τη μεταφόρτωση
          Οι μεταφορτώσιες θα είναι λίγο πιο αργές, αλλά ok.\nΣυγγνώμη για την ταλαιπωρία!\n\nPS: το chrome v107 έχει διόρθωση γι\' αυτό', "u_emtleakf": 'δοκίμασε τα εξής:\n
          • πάτα F5 για ανανέωση σελίδας
          • μετά άνοιξε το 🥔 (potato) στο UI μεταφόρτωσης
          • και δοκίμασε ξανά τη μεταφόρτωση
          \nPS: ο firefox ελπίζει να φτιάξει αυτό το bug κάποια στιγμή', "u_s404": "δεν βρέθηκε στο διακομιστή", "u_expl": "επεξήγηση", "u_maxconn": "οι περισσότεροι browser το περιορίζουν στα 6, αλλά ο firefox σου επιτρέπει να το αυξήσεις με connections-per-server στο about:config", "u_tu": '

          ΠΡΟΕΙΔΟΠΟΙΗΣΗ: το turbo είναι ενεργοποιημένο,  το πρόγραμμα πελάτη ίσως να μην ανιχνεύσει και να μην ξαναεκκινήσει μισοτελειωμένες μεταφορτώσεις; δες τα tooltip του κουμπιού turbo

          ', "u_ts": '

          ΠΡΟΕΙΔΟΠΟΙΗΣΗ: το turbo είναι ενεργοποιημένο,  τα αποτελέσματα αναζήτησης μπορεί να είναι λάθος; δες τα tooltip του κουμπιού turbo

          ', "u_turbo_c": "το turbo είναι απενεργοποιημένο στο αρχείο ρυθμίσεων του διακομιστή", "u_turbo_g": "απενεργοποιώ το turbo επειδή δεν έχεις δικαίωμα\nγια τη λίστα φακέλων σε αυτόν τον τόμο", "u_life_cfg": 'αυτόματη διαγραφή μετά από λεπτά (ή ώρες)', "u_life_est": 'η μεταφόρτωση θα διαγραφεί ---', "u_life_max": 'αυτός ο φάκελος επιβάλλει\nμέγιστη διάρκεια ζωής {0}', "u_unp_ok": "επιτρέπεται το unpost για {0}", "u_unp_ng": "δεν επιτρέπεται το unpost", "ue_ro": "έχεις μόνο δικαίωμα ανάγνωσης σε αυτόν το φάκελο\n\n", "ue_nl": "δεν είσαι συνδεδεμένος τώρα", "ue_la": 'είσαι συνδεδεμένος ως "{0}"', "ue_sr": "είσαι σε λειτουργία αναζήτησης αρχείων\n\nπήγαινε σε λειτουργία μεταφόρτωσης πατώντας το 🔎 (δίπλα στο μεγάλο κουμπί ΑΝΑΖΗΤΗΣΗΣ) και δοκίμασε πάλι\n\nσυγγνώμη", "ue_ta": "δοκίμασε να μεταφορτώσεις εκ νέου, θα πρέπει να δουλέψει τώρα", "ue_ab": "αυτό το αρχείο ανεβαίνει σε άλλο φάκελο και η μεταφόρτωση πρέπει να ολοκληρωθεί πριν ανέβει αλλού.\n\nΜπορείς να ακυρώσεις και να ξεχάσεις την αρχική μεταφόρτωση με το κουμπί 🧯 πάνω αριστερά", "ur_1uo": "ΟΚ: Το αρχείο ανέβηκε επιτυχώς", "ur_auo": "ΟΚ: Και τα {0} αρχεία ανέβηκαν επιτυχώς", "ur_1so": "ΟΚ: Το αρχείο βρέθηκε στο διακομιστή", "ur_aso": "ΟΚ: Και τα {0} αρχεία βρέθηκαν στο διακομιστή", "ur_1un": "Η μεταφόρτωση απέτυχε, συγγνώμη", "ur_aun": "Και οι {0} μεταφορτώσεις απέτυχαν, συγγνώμη", "ur_1sn": "Το αρχείο ΔΕΝ βρέθηκε στο διακομιστή", "ur_asn": "Τα {0} αρχεία ΔΕΝ βρέθηκαν στο διακομιστή", "ur_um": "Ολοκληρώθηκε;\n{0} μεταφορτώσεις είναι OK,\n{1} μεταφορτώσεις απέτυχαν, συγγνώμη", "ur_sm": "Ολοκληρώθηκε;\n{0} αρχεία βρέθηκαν στο διακομιστή,\n{1} αρχεία ΔΕΝ βρέθηκαν στο διακομιστή", "rc_opn": "άνοιγμα", //m "rc_ply": "αναπαραγωγή", //m "rc_pla": "αναπαραγωγή ως ήχος", //m "rc_txt": "άνοιγμα στον προβολέα αρχείων", //m "rc_md": "άνοιγμα στον επεξεργαστή κειμένου", //m "rc_dl": "λήψη", //m "rc_zip": "λήψη ως αρχείο", //m "rc_cpl": "ἀντίγραφε σύνδεσμον", //m "rc_del": "διαγραφή", //m "rc_cut": "αποκοπή", //m "rc_cpy": "αντιγραφή", //m "rc_pst": "επικόλληση", //m "rc_rnm": "μετονομασία", //m "rc_nfo": "νέος φάκελος", //m "rc_nfi": "νέο αρχείο", //m "rc_sal": "επιλογή όλων", //m "rc_sin": "αντιστροφή επιλογής", //m "rc_shf": "κοινή χρήση αυτού του φακέλου", //m "rc_shs": "κοινή χρήση επιλογής", //m "lang_set": "ανανέωση σελίδας για εφαρμογή της αλλαγής;", "splash": { "a1": "ανανέωση", "b1": "γεια σου ξένε!   (δεν είσαι συνδεδεμένος)", "c1": "αποσύνδεση", "d1": "λίστα διεργασιών", "d2": "εμφανίζει την κατάσταση όλων των ενεργών διεργασιών", "e1": "επαναφόρτωση του cfg", "e2": "φορτώνει ξανά τα αρχεία ρυθμίσεων (λογαριασμοί/τόμοι/volflags),$Nκαι κάνει επανεξέταση όλων των τόμων e2ds$N$Nσημείωση: οποιαδήποτε αλλαγή στις καθολικές ρυθμίσεις$Nαπαιτεί πλήρη επανεκκίνηση για να εφαρμοστεί", "f1": "μπορείς να περιηγηθείς:", "g1": "μπορείς να εκτελέσεις μεταφόρτωση σε:", "cc1": "άλλα πράγματα:", "h1": "απενεργοποίση k304", "i1": "ενεργοποίηση k304", "j1": "η ενεργοποίηση του k304 θα αποσυνδέσει το πρόγραμμα πελάτη σου σε κάθε HTTP 304, κάτι που μπορεί να αποτρέψει κάποια προβληματικά proxies από το να κολλάνε (να μην φορτώνουν ξαφνικά σελίδες), αλλά θα κάνει τα πράγματα, γενικά πιο αργά", "k1": "επαναφορά ρυθμίσεων στο πρόγραμμα πελάτη", "l1": "συνδέσου για περισσότερα:", "ls3": "σύνδεση", //m "lu4": "όνομα χρήστη", //m "lp4": "κωδικός πρόσβασης", //m "lo3": "αποσύνδεση του “{0}” από παντού", //m "lo2": "αυτό θα τερματίσει τη συνεδρία σε όλους τους περιηγητές", //m "m1": "καλώς ήρθες,", "n1": "404 δεν βρέθηκε  ┐( ´ -`)┌", "o1": '´η μήπως δεν έχεις πρόσβαση -- δοκίμασε έναν κωδικό πήγαινε στην αρχική', "p1": "403 απαγορευμένο  ~┻━┻", "q1": 'δοκίμασε έναν κωδικό πήγαινε στην αρχική', "r1": "πίσω στην αρχική", ".s1": "επανάληψη σάρωσης", "t1": "ενέργεια", "u2": "χρόνος από την τελευταία εγγραφή του διακομιστή$N( μεταφόρτωση / μετονομασία / ... )$N$N17d = 17 days$N1ω23 = 1 ώρα 23 λεπτά$N4λ56 = 4 λεπτά 56 δευτερόλεπτα", "v1": "σύνδεση", "v2": "χρησιμοποίησε αυτόν το διακομιστή σαν τοπικό δίσκο", "w1": "εναλλαγή σε https", "x1": "αλλαγή κωδικού", "y1": "επεξεργασία κοινόχρηστων φακέλων", "z1": "ξεκλείδωμα αυτού του κοινόχρηστου φακέλου:", "ta1": "συμπλήρωσε πρώτα το νέο σου κωδικό", "ta2": "επανέλαβε για να επιβεβαιώσεις το νέο κωδικό:", "ta3": "βρέθηκε τυπογραφικό λάθος· δοκίμασε ξανά", "nop": "ΣΦΑΛΜΑ: Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός", //m "nou": "ΣΦΑΛΜΑ: Το όνομα χρήστη και/ή ο κωδικός πρόσβασης δεν μπορεί να είναι κενό", //m "aa1": "εισερχόμενα αρχεία:", "ab1": "απενεργοποίηση no304", "ac1": "ενεργοποίηση no304", "ad1": "η ενεργοποίηση του no304 θα απενεργοποιήσει όλη την προσωρινή αποθήκευση· δοκίμασέ το αν το k304 δεν ήταν αρκετό. Προσοχή, θα σπαταλήσει τεράστιο όγκο δικτυακής κίνησης!", "ae1": "ενεργές μεταφορτώσεις:", "af1": "προβολή πρόσφατων μεταφορτώσεων", "ag1": "εμφάνιση γνωστών χρηστών IdP", //m } }; ================================================ FILE: copyparty/web/tl/hun.js ================================================ Ls.hun = { "tt": 'Magyar', "cols": { "c": 'műveletek', "dur": 'hossz', "q": 'minőség / bitrate', "Ac": 'audió kodek', "Vc": 'videó kodek', "Fmt": 'konténer', "Ahash": 'audió hash', "Vhash": 'videó hash', "Res": 'felbontás', "T": 'típus', "aq": 'audió minőség / bitrate', "vq": 'videó minőség / bitrate', "pixfmt": 'színkódolás / pixel', "resw": 'szélesség', "resh": 'magasság', "chs": 'csatornák', "hz": 'mintavételezés', }, "hks": [ [ 'egyéb', ['ESC', 'minden bezárása'], 'fájlkezelő', ['G', 'lista / rács nézet'], ['T', 'ikon / indexkép váltás'], ['⇧ A/D', 'méret módosítása'], ['ctrl-K', 'kijelöltek törlése'], ['ctrl-X', 'kivágás vágólapra'], ['ctrl-C', 'másolás vágólapra'], ['ctrl-V', 'beillesztés ide'], ['Y', 'kijelöltek letöltése'], ['F2', 'átnevezés'], 'kijelölés', ['space', 'fájl kijelölése'], ['↑/↓', 'kurzor mozgatása'], ['ctrl ↑/↓', 'kurzor + nézet mozgatása'], ['⇧ ↑/↓', 'fájlok kijelölése'], ['ctrl-A', 'mindet kijelöli'], ], [ 'navigáció', ['B', 'fa / breadcrumbs váltás'], ['I/K', 'előző/következő mappa'], ['M', 'vissza a főmappába'], ['V', 'fájlok mutatása az oldalsávon'], ['A/D', 'oldalsáv mérete'], ], [ 'zenelejátszó', ['J/L', 'előző/következő szám'], ['U/O', '10 mp tekerés (vissza/előre)'], ['0..9', 'ugrás (0%..90%)'], ['P', 'play/pause'], ['S', 'épp szóló szám kijelölése'], ['Y', 'szám letöltése'], ], [ 'képnézegető', ['J/L, ←/→', 'előző/következő kép'], ['Home/End', 'első/utolsó kép'], ['F', 'teljes képernyő'], ['R', 'forgatás jobbra'], ['⇧ R', 'forgatás balra'], ['S', 'kép kijelölése'], ['Y', 'kép letöltése'], ], [ 'videólejátszó', ['U/O', '10 mp tekerés (vissza/előre)'], ['P/K/Space', 'play/pause'], ['C', 'folyamatos lejátszás'], ['V', 'ismétlés'], ['M', 'némítás'], ['[ és ]', 'ismétlési tartomány'], ], [ 'szövegszerkesztő', ['I/K', 'előző/következő fájl'], ['M', 'bezárás'], ['E', 'szerkesztés'], ['S', 'kijelöl fv művelethez'], ['Y', 'letöltés'], ['⇧ J', 'json formázás (beautify)'], ], ], "m_ok": 'ok', "m_ng": 'mégse', "enable": 'bekapcsolva', "danger": 'FIGYELEM', "clipped": 'vágólapra másolva', "ht_s1": 'másodperce', "ht_s2": 'másodperce', "ht_m1": 'perce', "ht_m2": 'perce', "ht_h1": 'órája', "ht_h2": 'órája', "ht_d1": 'napja', "ht_d2": 'napja', "ht_and": ' és ', "goh": 'irányítópult', "gop": 'előző mappába">előző', "gou": 'szülőmappa">fel', "gon": 'következő mappába">következő', "logout": 'Kilépés ', "login": 'Belépés', "access": ' hozzáférés', "ot_close": 'almenü bezárása', "ot_search": '`keresés attribútumok, név, elérési út vagy metaadatok alapján$N$N`valami más` = szerepelnie kell mindkettőnek,$N`valami -más` = tartalmazza a «valami»-t, de a «más»-t nem,$N`^kezd .mp3$` = «kezd»-del induló mp3 fájlok$N`"pontosan ez"` = pontos egyezés$N$Ndátum formátum iso-8601, pl.$N`2024-12-31` vagy `2024-09-12 23:30:00`', "ot_unpost": 'töröld a nemrég feltöltött fájljaidat, vagy állítsd le a folyamatban lévőket', "ot_bup": 'bup: egyszerű feltöltő (még netscape 4-en is megy)', "ot_mkdir": 'új mappa létrehozása', "ot_md": 'új szöveges fájl', "ot_msg": 'üzenet küldése a szerver logba', "ot_mp": 'lejátszó beállításai', "ot_cfg": 'beállítások', "ot_u2i": 'up2k: fájlok feltöltése (ha van írási jogod) vagy keresés a szerveren$N$Nfolytatható, többszálú, dátumokat megőrzi, de CPU-éhesebb az egyszerű [🎈]-nél.

          feltöltés közben az ikon mutatja a haladást!', "ot_u2w": 'up2k: feltöltés folytatási támogatással (böngésző bezárás után is folytathatod)$N$Ntöbbszálú, dátumokat megőrzi, de CPU-éhesebb az egyszerűnél.

          feltöltés közben az ikon mutatja a haladást!', "ot_noie": 'Kérlek használj Chrome-ot / Firefox-ot / Edge-et', "ab_mkdir": 'mappa létrehozása', "ab_mkdoc": 'új szövegfájl', "ab_msg": 'log üzenet küldése', "ay_path": 'ugrás a mappákhoz', "ay_files": 'ugrás a fájlokhoz', "wt_ren": 'átnevezés$Ngyorsbillentyű: F2', "wt_del": 'törlés$Ngyorsbillentyű: ctrl-K', "wt_cut": 'kivágás <small>(beillesztéshez)</small>$Ngyorsbillentyű: ctrl-X', "wt_cpy": 'másolás vágólapra$Ngyorsbillentyű: ctrl-C', "wt_pst": 'beillesztés$Ngyorsbillentyű: ctrl-V', "wt_selall": 'összes kijelölése$Ngyorsbillentyű: ctrl-A', "wt_selinv": 'kijelölés megfordítása', "wt_zip1": 'mappa letöltése zip/tar-ként', "wt_selzip": 'kijelöltek letöltése archívumként', "wt_seldl": 'kijelöltek letöltése külön fájlokként$Ngyorsbillentyű: Y', "wt_npirc": 'irc-formátumú infó másolása', "wt_nptxt": 'egyszerű szöveges infó másolása', "wt_m3ua": 'hozzáadás m3u listához (később 📻másolás)', "wt_m3uc": 'm3u lista másolása vágólapra', "wt_grid": 'lista / rács nézet váltás$Ngyorsbillentyű: G', "wt_prev": 'előző szám$Ngyorsbillentyű: J', "wt_play": 'lejátszás / szünet$Ngyorsbillentyű: P', "wt_next": 'következő szám$Ngyorsbillentyű: L', "ul_par": 'párhuzamos szálak:', "ut_rand": 'véletlen fájlnevek', "ut_u2ts": 'helyi dátumok$Nátvitele a szerverre">📅', "ut_ow": 'felülírás?$N🛡️: soha (új nevet ad)$N🕒: csak ha a tiéd újabb$N♻️: mindig, ha más a tartalom$N⏭️: létezőket kihagy', "ut_mt": 'háttérben hashelés feltöltés alatt$N$Nkapcsold ki, ha fagy a géped', "ut_ask": 'megerősítés feltöltés előtt">💭', "ut_pot": 'feltöltés gyorsítása (egyszerűbb UI)', "ut_srch": 'csak létezés ellenőrzése$N(nem tölt fel semmit)', "ut_par": '0 = szünet$N$Nnöveld, ha lassú a net$N$NHDD limit vagy LAN esetén hagyd 1-en', "ul_btn": 'dobd ide a fájlokat/mappákat
          (vagy kattints)', "ul_btnu": 'F E L T Ö L T É S', "ul_btns": 'K E R E S É S', "ul_hash": 'hash', "ul_send": 'küldés', "ul_done": 'kész', "ul_idle1": 'nincs várakozó feltöltés', "ut_etah": 'átlagos <em>hashelési</em> sebesség és becsült idő', "ut_etau": 'átlagos <em>feltöltési</em> sebesség és becsült idő', "ut_etat": 'átlagos <em>össz</em> sebesség és becsült idő', "uct_ok": 'sikerült', "uct_ng": 'hiba / elutasítva', "uct_done": 'összesen', "uct_bz": 'hashelés vagy feltöltés...', "uct_q": 'várakozik', "utl_name": 'fájlnév', "utl_ulist": 'lista', "utl_ucopy": 'másolás', "utl_links": 'linkek', "utl_stat": 'státusz', "utl_prog": 'haladás', "utl_404": '404', "utl_err": 'HIBA', "utl_oserr": 'OS-hiba', "utl_found": 'megvan', "utl_defer": 'halasztva', "utl_yolo": 'YOLO', "utl_done": 'kész', "ul_flagblk": 'fájlok a várólistán
          egy másik lapon már fut egy feltöltés,
          megvárjuk, amíg az végez', "ul_btnlk": 'szerveroldali korlátozás miatt nem módosítható', "udt_up": 'Feltöltés', "udt_srch": 'Keresés', "udt_drop": 'dobd ide', "u_nav_m": '
          mit töltsünk fel?
          Enter = Fájlok (egy vagy több)\nESC = Egy mappa (almappákkal)', "u_nav_b": 'FájlokEgy mappa', "cl_opts": 'beállítások', "cl_hfsz": 'méret', "cl_themes": 'téma', "cl_langs": 'nyelv', "cl_ziptype": 'mappa letöltés', "cl_uopts": 'up2k beállítások', "cl_favico": 'favicon', "cl_bigdir": 'limit feletti mappák', "cl_hsort": '#rendezés', "cl_keytype": 'hangnem-jelölés', "cl_hiddenc": 'rejtett oszlopok', "cl_hidec": 'rejt', "cl_reset": 'alaphelyzet', "cl_hpick": 'kattints az oszlopfejlécre az elrejtéshez', "cl_hcancel": 'elrejtés megszakítva', "cl_rcm": 'jobb-klikkes menü', "ct_grid": '田 rács nézet', "ct_ttips": '◔ ◡ ◔">ℹ️ segítő szövegek', "ct_thumb": 'rács nézetben ikonok/indexképek váltása$Ngyorsbillentyű: T">🖼️ képek', "ct_csel": 'kijelölés CTRL és SHIFT gombokkal rács nézetben">kijelölés', "ct_dsel": 'kijelölés egérhúzással rács nézetben">húzás', "ct_dl": 'azonnali letöltés (beágyazás helyett)">letöltés', "ct_ihop": 'nézegető bezárásakor ugorjon az utolsó képhez">g⮯', "ct_dots": 'rejtett fájlok mutatása (ha szabad)">rejtett', "ct_qdel": 'csak egyszer kérdezzen rá a törlésre">gyorstörlés', "ct_dir1st": 'mappák előre rendezése">📁 előre', "ct_nsort": 'természetes rendezés (pl. 1, 2, 10)">rendezés', "ct_utc": 'időpontok UTC-ben">UTC', "ct_readme": 'README.md mutatása a mappákban">📜 readme', "ct_idxh": 'index.html mutatása listázás helyett">htm', "ct_sbars": 'gördítősávok mutatása">⟊', "cut_umod": 'létező fájl dátumának frissítése a helyire (írható+törölhető jog kell)">re📅', "cut_turbo": 'yolo gomb, nem ajánlott:$N$Nrengeteg fájl újraellenőrzéséhez jó$N$Ncsak méretet néz, tartalmat nem! ha a méret egyezik, NEM tölti újra, még ha a fájl más is.$N$Nha kész vagy, kapcsold ki!">turbo', "cut_datechk": 'csak turbo módban számít$N$Nnézi a dátumokat is a méret mellé$N$Nvalamivel biztosabb a turbónál, de nem váltja ki a rendes ellenőrzést">dátum-chk', "cut_u2sz": 'feltöltési egység (chunk) mérete (MiB); nagy érték jó a távoli szerverekhez, kicsi a gyenge nethez', "cut_flag": 'egyszerre csak egy fül tölthessen fel (azonos domainen belül hat)', "cut_az": 'fájlok feltöltése ABC-sorrendben (nem a legkisebb-először módon)', "cut_nag": 'értesítés, ha kész a feltöltés (ha nem ezen a fülön állsz)', "cut_sfx": 'hangjelzés, ha kész a feltöltés', "cut_mt": 'többszálú hashelés (gyorsítás)$N$Nplusz RAM-ot kér (max 512 MiB)$N$Nhttps-t 30%-kal, http-t 4.5x gyorsítja">mt', "cut_wasm": 'wasm hashelő; chrome-on gyorsabb, de CPU-igényes és régebbi verzióknál fagyhat">wasm', "cft_text": 'favicon szöveg (üresen kikapcsol)', "cft_fg": 'szöveg színe', "cft_bg": 'háttér színe', "cdt_lim": 'lista hossza (max fájl/mappa)', "cdt_ask": 'lista végén ne töltsön be automatikusan, inkább kérdezzen rá', "cdt_hsort": 'rendezési szabályok száma a linkekben. 0 = tiltás', "cdt_ren": 'egyedi jobb-klikkes menü (gyári: SHIFT + jobb-klikk)">bekapcsolva', "cdt_rdb": 'mutassa a böngészőnek jobb-klikkes menüjét, ha az egyedi már nyitva van és megint jobb-klikkelsz">dupla', "tt_entree": 'oldalsáv (mappafa) mutatása$Ngyorsbillentyű: B', "tt_detree": 'elérési út mutatása$Ngyorsbillentyű: B', "tt_visdir": 'gördítés a kijelölt mappához', "tt_ftree": 'mappák / szövegfájlok váltása$Ngyorsbillentyű: V', "tt_pdock": 'szülőmappák rögzítése felül', "tt_dynt": 'automatikus méretezés nyitáskor', "tt_wrap": 'sortörés', "tt_hover": 'túl hosszú sorok mutatása rámutatáskor', "ml_pmode": 'mappa végén...', "ml_btns": 'gombok', "ml_tcode": 'átkódolás', "ml_tcode2": 'átkódolás ide:', "ml_tint": 'színezés', "ml_eq": 'eq', "ml_drc": 'dinamikatartomány-tömörítő', "ml_ss": 'csönd átugrása', "mt_loop": 'egy szám ismétlése">🔁', "mt_one": 'leállás egy szám után">1️⃣', "mt_shuf": 'véletlenszerű lejátszás mappánként">🔀', "mt_aplay": 'automatikus kezdés a link alapján$N$Nha kikapcsolod, a linkek sem fognak szám-azonosítót tartalmazni">a▶', "mt_preload": 'következő szám betöltése a végén (gapless)">preload', "mt_prescan": 'mappaváltás az utolsó szám vége előtt, hogy a böngésző ne álljon le">nav', "mt_fullpre": 'teljes szám előtöltése;$N✅ instabil netre,$N❌ lassú neten inkább kapcsold ki">full', "mt_fau": 'mobilon megakadályozza a leállást, ha lassú az előtöltés (a tagek kijelzése hibás lehet)">☕️', "mt_waves": 'hullámforma a tekerősávon:$Nhangerő amplitúdó mutatása a sávban">~s', "mt_npclip": 'gombok a most szóló szám infójának másolásához">/np', "mt_m3u_c": 'gombok a kijelöltek m3u8 listába másolásához">📻', "mt_octl": 'rendszerintegráció (média gombok / osd)">os-ctl', "mt_oseek": 'tekerés engedése a rendszer szinten$N$Nmegjegyzés: iphonon ez felülírja a következő szám gombot">seek', "mt_oscv": 'albumkép mutatása az osd-n">art', "mt_follow": 'fókuszban tartja az aktuális számot">🎯', "mt_compact": 'kompakt vezérlők">⟎', "mt_uncache": 'gyorsítótár ürítése (próbáld ezt, ha a böngésző hibás fájlt mentett el és nem játssza le)">uncache', "mt_mloop": 'mappa ismétlése">🔁 ismétlés', "mt_mnext": 'következő mappa betöltése és folytatás">📂 tovább', "mt_mstop": 'stop">⏸ stop', "mt_cflac": 'flac / wav konvertálása ide: {0}">flac', "mt_caac": 'aac / m4a konvertálása ide: {0}">aac', "mt_coth": 'egyéb (nem mp3) konvertálása ide: {0}">egyéb', "mt_c2opus": 'asztali gép, laptop, android kedvence">opus', "mt_c2owa": 'opus-weba (iOS 17.5+)">owa', "mt_c2caf": 'opus-caf (iOS 11-17)">caf', "mt_c2mp3": 'nagyon régi eszközökhöz">mp3', "mt_c2flac": 'legjobb minőség, de hatalmas fájlok">flac', "mt_c2wav": 'tömörítetlen (még nagyobb)">wav', "mt_c2ok": 'szuper, jó választás', "mt_c2nd": 'nem javasolt ehhez az eszközhöz, de rendben', "mt_c2ng": 'úgy tűnik, az eszközöd nem támogatja, de próbáljuk meg', "mt_xowa": 'iOS hiba akadályozza a háttérben lejátszást ezzel; használd inkább a caf-ot vagy mp3-at', "mt_tint": 'háttérszint (0-100) a tekerősávon, hogy ne zavarjon a betöltés', "mt_eq": '`eq és erősítés;$N$Nboost `0` = alap hangerő (100%)$N$Nszélesség `1  ` = standard sztereó$Nszélesség `0.5` = 50% áthallás$Nszélesség `0  ` = monó$N$Nboost `-0.8` & szélesség `10` = ének eltávolítása :^)$N$Nha az EQ be van kapcsolva, a lemezek teljesen folytonosak lesznek (gapless), hagyd bekapcsolva nullán is, ha ez számít', "mt_drc": 'dinamikatartomány-tömörítés (hangerő-kiegyenlítő); az EQ-t is bekapcsolja, ha nem kell, az értékeit hagyd nullán (kivéve szélesség=1)$N$Nevvel sosem lesz túl hangos, -24 thresh és 12 ratio mellett -22 dB-nél megáll, biztonságos feltekerni a boost-ot 0.8-ra vagy akár 1.8-ra', "mt_ss": '`csönd átugrása; felgyorsít `ffwd`-szeresére a számok elején/végén, ha a hangerő `vol` alatt van és a pozíció a szám első/utolsó `start/end` százalékában van', "mt_ssvt": 'hangerő küszöb (0-255)">vol', "mt_ssts": 'aktív tartomány (%, eleje)">start', "mt_sste": 'aktív tartomány (%, vége)">end', "mt_sssm": 'lejátszási sebesség szorzó">ffwd', "mb_play": 'play', "mm_hashplay": 'lejátsszam ezt a fájlt?', "mm_m3u": 'Enter/OK: Lejátszás\nESC/Cancel: Szerkesztés', "mp_breq": "firefox 82+, chrome 73+, vagy iOS 15+ szükséges", "mm_bload": 'betöltés...', "mm_bconv": 'konvertálás ide: {0}, várj légy szíves...', "mm_opusen": 'a böngésződ nem támogatja az aac / m4a fájlokat;\nátkódolás opus-ba bekapcsolva', "mm_playerr": 'lejátszási hiba: ', "mm_eabrt": 'lejátszás megszakítva', "mm_enet": 'szakadozik a neted', "mm_edec": 'ez a fájl valahogy sérült??', "mm_esupp": 'a böngésződ nem ismeri ezt a formátumot', "mm_eunk": 'ismeretlen hiba', "mm_e404": 'audió hiba 404: fájl nem található.', "mm_e403": 'audió hiba 403: hozzáférés megtagadva.\n\npróbáld meg az F5-öt, hátha kiléptetett a rendszer', "mm_e415": 'audió hiba 415: átkódolás sikertelen; nézd meg a szerver logot.', "mm_e500": 'audió hiba 500: nézd meg a szerver logot.', "mm_e5xx": 'audió hiba; szerver hiba ', "mm_nof": 'nem találok több zenét a környéken', "mm_prescan": 'következő szám keresése...', "mm_scank": 'következő megvan:', "mm_uncache": 'gyorsítótár ürítve; minden számot újra letölt az újabb lejátszásnál', "mm_hnf": 'ez a szám már nem létezik', "im_hnf": 'ez a kép már nem létezik', "f_empty": 'a mappa üres', "f_chide": 'ez elrejti a(z) «{0}» oszlopot\n\nvisszakapcsolhatod a beállításoknál', "f_bigtxt": 'ez a fájl {0} MiB -- valóban megnyitod szövegként?', "f_bigtxt2": 'csak a fájl végét nézed meg? ez bekapcsolja a követést (tail) is, az új sorok élőben jelennek meg', "fbd_more": '
          {0} / {1} fájl látszik; mutass még {2}-t vagy mutasd mindet
          ', "fbd_all": '
          {0} / {1} fájl látszik; mutasd mindet
          ', "f_anota": 'csak {0} fájl van kijelölve a {1}-ből;\naz egész mappa kijelöléséhez görgess az aljára', "f_dls": 'a mappában lévő linkek letöltési linkekké alakultak', "f_dl_nd": 'mappa kihagyása (használd a zip/tar letöltést):\n', "f_partial": 'egy épp feltöltés alatt álló fájlt akarsz letölteni. Inkább válaszd azt, aminek megegyezik a neve, de nincs rajta a .PARTIAL kiterjesztés.\n\nha mégis OK-t nyomsz, valószínűleg hibás adatot kapsz.', "ft_paste": 'beillesztés: {0} elem$Ngyorsbillentyű: ctrl-V', "fr_eperm": 'nem tudom átnevezni:\nnincs “mozgatás” jogod ebben a mappában', "fd_eperm": 'nem tudom törölni:\nnincs “törlés” jogod ebben a mappában', "fc_eperm": 'nem tudom kivágni:\nnincs “ mozgatás ” jogod ebben a mappában', "fp_eperm": 'nem tudom beilleszteni:\nnincs “írás” jogod ebben a mappában', "fr_emore": 'jelölj ki legalább egy elemet az átnevezéshez', "fd_emore": 'jelölj ki legalább egy elemet a törléshez', "fc_emore": 'jelölj ki legalább egy elemet a kivágáshoz', "fcp_emore": 'jelölj ki legalább egy elemet a másoláshoz', "fs_sc": 'mappa megosztása', "fs_ss": 'kijelöltek megosztása', "fs_just1d": 'nem tudsz egyszerre több mappát,\nvagy fájlokat és mappákat vegyesen megosztani', "fs_abrt": '❌ mégse', "fs_rand": '🎲 random név', "fs_go": '✅ megosztás létrehozása', "fs_name": 'név', "fs_src": 'forrás', "fs_pwd": 'jelszó', "fs_exp": 'lejárat', "fs_tmin": 'perc', "fs_thrs": 'óra', "fs_tdays": 'nap', "fs_never": 'örök', "fs_pname": 'nem kötelező link név; üresen véletlenszerű lesz', "fs_tsrc": 'a megosztandó fájl vagy mappa', "fs_ppwd": 'nem kötelező jelszó', "fs_w8": 'megosztás létrehozása...', "fs_ok": 'Enter/OK: Másolás vágólapra\nESC/Cancel: Bezárás', "frt_dec": 'helyreállíthatja a hibás fájlneveket">url-decode', "frt_rst": 'eredeti nevek visszaállítása">↺ alaphelyzet', "frt_abrt": 'ablak bezárása">❌ mégse', "frb_apply": 'ÁTNEVEZÉS', "fr_adv": 'tömeges / metaadat / minta alapú átnevezés">haladó', "fr_case": 'kis/nagybetű érzékeny regex">case', "fr_win": 'windows-biztos nevek; cseréli a <>:"\\|?* karaktereket japán teljes szélességűekre">win', "fr_slash": '/ cseréje olyanra, ami nem hoz létre új mappát">no /', "fr_re": '`regex keresési minta; a csoportokra így hivatkozhatsz lent: `(1)`, `(2)` stb.', "fr_fmt": '`foobar2000 stílus:$N`(title)` = zeneszám címe,$N`[(artist) - ](title)` = kihagyja az előadót, ha üres$N`$lpad((tn),2,0)` = sorszám 2 számjegyen', "fr_pdel": 'törlés', "fr_pnew": 'mentés mint', "fr_pname": 'adj nevet az új presetnek', "fr_aborted": 'megszakítva', "fr_lold": 'régi név', "fr_lnew": 'új név', "fr_tags": 'tagek a kijelölt fájlokban (csak olvasásra):', "fr_busy": '{0} elem átnevezése...\n\n{1}', "fr_efail": 'átnevezés sikertelen:\n', "fr_nchg": '{0} név módosult a win vagy no / miatt\n\nfolytassuk az új nevekkel?', "fd_ok": 'törölve', "fd_err": 'törlés sikertelen:\n', "fd_none": 'semmi sem lett törölve; lehet, hogy a szerver tiltja (xbd)?', "fd_busy": '{0} elem törlése...\n\n{1}', "fd_warn1": 'TÖRLÖD ezt a(z) {0} elemet?', "fd_warn2": 'Utolsó esély! Nem vonható vissza. Törlöd?', "fc_ok": '{0} elem kivágva', "fc_warn": '{0} elem kivágva\n\nde: csak ez a fül tudja beilleszteni őket\n(mivel a kijelölés brutálisan nagy)', "fcc_ok": '{0} elem vágólapra másolva', "fcc_warn": '{0} elem vágólapra másolva\n\nde: csak ez a fül tudja beilleszteni őket\n(mivel a kijelölés brutálisan nagy)', "fp_apply": 'nevek mentése', "fp_skip": 'ütközések kihagyása', "fp_ecut": 'előbb vágj ki vagy másolj valamit\n\nmegjegyzés: néha fülek között is megy', "fp_ename": '{0} elem nem mozgatható ide névütközés miatt. Adj nekik új nevet, vagy hagyd üresen (“kihagyás”) a skippeléshez:', "fcp_ename": '{0} elem nem másolható ide névütközés miatt. Adj nekik új nevet, vagy hagyd üresen (“kihagyás”) a skippeléshez:', "fp_emore": 'maradt még néhány névütközés', "fp_ok": 'mozgatás kész', "fcp_ok": 'másolás kész', "fp_busy": '{0} elem mozgatása...\n\n{1}', "fcp_busy": '{0} elem másolása...\n\n{1}', "fp_abrt": 'megszakítás...', "fp_err": 'mozgatás sikertelen:\n', "fcp_err": 'másolás sikertelen:\n', "fp_confirm": 'mozgassuk ide ezt a {0} elemet?', "fcp_confirm": 'másoljuk ide ezt a {0} elemet?', "fp_etab": 'nem sikerült a vágólap olvasása másik fülről', "fp_name": 'fájl feltöltése. Adj neki nevet:', "fp_both_m": '
          mi legyen?
          Enter = Mozgatás: {0} elem innen: «{1}»\nESC = Feltöltés: {2} fájl a gépedről', "fcp_both_m": '
          mi legyen?
          Enter = Másolás: {0} elem innen: «{1}»\nESC = Feltöltés: {2} fájl a gépedről', "fp_both_b": 'MozgatásFeltöltés', "fcp_both_b": 'MásolásFeltöltés', "mk_noname": 'írj be egy nevet a bal oldali mezőbe :p', "nmd_i1": 'add meg a kiterjesztést is, pl. .md', "nmd_i2": 'csak .{0} fájlt hozhatsz létre, mert nincs törlési jogod', "tv_load": 'Szövegfájl betöltése:\n\n{0}\n\n{1}% ({2} / {3} MiB betöltve)', "tv_xe1": 'nem sikerült betölteni:\n\nhiba ', "tv_xe2": '404, fájl nem található', "tv_lst": 'szövegfájlok a mappában:', "tvt_close": 'vissza a mappába$Ngyorsbillentyű: M (vagy Esc)">❌ bezárás', "tvt_dl": 'fájl letöltése$Ngyorsbillentyű: Y">💾 letöltés', "tvt_prev": 'előző dokumentum$Ngyorsbillentyű: i">⬆ előző', "tvt_next": 'következő dokumentum$Ngyorsbillentyű: K">⬇ következő', "tvt_sel": 'kijelölés (vágáshoz/másoláshoz/törléshez)$Ngyorsbillentyű: S">kijelöl', "tvt_j": 'json formázás$Ngyorsbillentyű: shift-J">j', "tvt_edit": 'szerkesztés szövegként$Ngyorsbillentyű: E">✏️ szerkeszt', "tvt_tail": 'fájl figyelése; új sorok mutatása élőben">📡 követve', "tvt_wrap": 'sortörés">↵', "tvt_atail": 'gördítés az aljára rögzítve">⚓', "tvt_ctail": 'terminál színek (ansi escape) dekódolása">🌈', "tvt_ntail": 'visszapörgetési limit (hány bájt szöveg maradjon a memóriában)', "m3u_add1": 'szám hozzáadva az m3u listához', "m3u_addn": '{0} szám hozzáadva az m3u listához', "m3u_clip": 'm3u lista vágólapra másolva\n\nhozz létre egy új szövegfájlt valami.m3u névvel és illeszd bele; így már lejátszható lesz', "gt_vau": 'videó elrejtése, csak a hang lejátszása">🎧', "gt_msel": 'kijelölés engedélyezése; ctrl-klikk a felülbíráláshoz$N$N<em>ha aktív: dupla klikk a megnyitáshoz</em>$N$Ngyorsbillentyű: S">tömeges', "gt_crop": 'középre vágott indexképek">crop', "gt_3x": 'nagy felbontású indexképek">3x', "gt_zoom": 'zoom', "gt_chop": 'vágás', "gt_sort": 'rendezés:', "gt_name": 'név', "gt_sz": 'méret', "gt_ts": 'dátum', "gt_ext": 'típus', "gt_c1": 'rövidebb fájlnevek', "gt_c2": 'hosszabb fájlnevek', "sm_w8": 'keresés...', "sm_prev": 'az alábbi eredmények egy korábbi keresésből vannak:\n ', "sl_close": 'eredmények bezárása', "sl_hits": '{0} találat', "sl_moar": 'továbbiak betöltése', "s_sz": 'méret', "s_dt": 'dátum', "s_rd": 'útvonal', "s_fn": 'név', "s_ta": 'tagek', "s_ua": 'feltöltve', "s_ad": 'haladó', "s_s1": 'minimum MiB', "s_s2": 'maximum MiB', "s_d1": 'min. iso8601', "s_d2": 'max. iso8601', "s_u1": 'ezután töltve', "s_u2": 'vagy ez előtt', "s_r1": 'útvonal tartalmazza (szóközzel elválasztva)', "s_f1": 'név tartalmazza (negálás pl -nemez)', "s_t1": 'tagek tartalmazzák (^=kezdet, vég=$)', "s_a1": 'specifikus metaadatok', "md_eshow": 'nem sikerült megjeleníteni ', "md_off": '[📜readme] kikapcsolva a beállításokban -- rejtve', "badreply": 'Hibás válasz a szervertől', "xhr403": '403: Hozzáférés megtagadva\n\npróbáld az F5-öt, hátha kiléptetett a rendszer', "xhr0": 'ismeretlen hiba (valószínűleg megszakadt a kapcsolat)', "cf_ok": 'bocsi, DD' + wah + 'oS védelem bekapcsolt\n\nkb. 30 mp múlva folytatódik\n\nha nem történik semmi, nyomj F5-öt', "tl_xe1": 'almappák listázása sikertelen:\n\nhiba ', "tl_xe2": '404: mappa nem található', "fl_xe1": 'fájlok listázása sikertelen:\n\nhiba ', "fl_xe2": '404: mappa nem található', "fd_xe1": 'almappa létrehozása sikertelen:\n\nhiba ', "fd_xe2": '404: szülőmappa nincs meg', "fsm_xe1": 'üzenetküldés sikertelen:\n\nhiba ', "fsm_xe2": '404: szülőmappa nincs meg', "fu_xe1": 'unpost lista betöltése sikertelen:\n\nhiba ', "fu_xe2": '404: fájl nincs meg??', "fz_tar": 'tömörítetlen gnu-tar (linux / mac)', "fz_pax": 'tömörítetlen pax-formátumú tar (lassabb)', "fz_targz": 'gnu-tar gzip 3-as szinten$N$Náltalában lassú, inkább használd a sima tar-t', "fz_tarxz": 'gnu-tar xz 1-es szinten$N$Náltalában lassú, inkább használd a sima tar-t', "fz_zip8": 'zip utf8 nevekkel (régi windows alatt hibás lehet)', "fz_zipd": 'zip cp437 nevekkel (nagyon régi szoftverekhez)', "fz_zipc": 'cp437 crc32 gyorsítással,$NMS-DOS PKZIP v2.04g-hez (1993 októbere)$N(letöltés előtt hosszabb ideig tarthat a feldolgozás)', "un_m1": 'itt törölheted a nemrég feltöltött fájljaidat (vagy leállíthatod a futókat)', "un_upd": 'frissítés', "un_m4": 'vagy oszd meg az alábbi fájlokat:', "un_ulist": 'mutasd', "un_ucopy": 'másold', "un_flt": 'szűrő:  az URL tartalmazza:', "un_fclr": 'szűrő törlése', "un_derr": 'törlés sikertelen:\n', "un_f5": 'valami elromlott, próbáld meg frissíteni az oldalt', "un_uf5": 'frissítened kell az oldalt (F5 vagy CTRL-R), mielőtt ezt leállíthatnád', "un_nou": 'figyelem: a szerver túlterhelt a futó feltöltések listázásához; próbáld később', "un_noc": 'figyelem: a kész feltöltések törlése nincs engedélyezve a szerveren', "un_max": 'az első 2000 fájl látszik (használd a szűrőt)', "un_avail": '{0} kész feltöltés törölhető
          {1} folyamatban lévő leállítható', "un_m2": 'idő szerint rendezve (legfrissebb legelöl):', "un_no1": 'semmi! nincsenek nemrég feltöltött fájljaid', "un_no2": 'semmi! nincs a szűrőnek megfelelő friss feltöltés', "un_next": 'következő {0} fájl törlése', "un_abrt": 'leállítás', "un_del": 'törlés', "un_m3": 'legutóbbi feltöltések betöltése...', "un_busy": '{0} fájl törlése...', "un_clip": '{0} link vágólapra másolva', "u_https1": 'érdemesebb lenne', "u_https2": 'https-re váltani', "u_https3": 'a jobb sebességért', "u_ancient": 'lenyűgözően őskori a böngésződ -- talán használd inkább az egyszerű uploader-t', "u_nowork": 'legalább Firefox 53, Chrome 57 vagy iOS 11 kell', "tail_2old": 'legalább Firefox 105, Chrome 71 vagy iOS 14.5 kell', "u_nodrop": 'a böngésződ túl öreg a drag-and-drop-hoz', "u_notdir": 'ez nem egy mappa!\n\na böngésződ túl öreg,\npróbáld meg inkább behúzni', "u_uri": 'képek behúzásához más ablakból,\ndobd rá a nagy feltöltés gombra', "u_enpot": 'váltás egyszerű UI-ra (gyorsíthat a feltöltésen)', "u_depot": 'váltás vibráló UI-ra (visszafoghatja a sebességet)', "u_gotpot": 'átváltottunk egyszerű UI-ra a sebesség miatt,\nde bármikor visszaválthatsz!', "u_pott": '

          fájlok:   {0} kész,   {1} hiba,   {2} fut,   {3} vár

          ', "u_ever": 'ez az egyszerű feltöltő; az up2k-hoz legalább
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1 kell', "u_su2k": 'ez az egyszerű feltöltő; az up2k sokkal jobb', "u_uput": 'sebességre optimalizál (ellenőrzés nélkül)', "u_ewrite": 'nincs írási jogod ebben a mappában', "u_eread": 'nincs olvasási jogod ebben a mappában', "u_enoi": 'fájlkeresés letiltva a szerveren', "u_enoow": 'felülírás nem fog menni; törlési jog kell hozzá', "u_badf": 'Ez a(z) {0} fájl ({1}-ből) kimaradt, valószínűleg jogok miatt:\n\n', "u_blankf": 'Ez a(z) {0} fájl ({1}-ből) teljesen üres; biztos feltöltöd?\n\n', "u_applef": 'Ez a(z) {0} fájl ({1}-ből) valószínűleg felesleges;\nNyomj OK/Enter-t a KIHAGYÁSHOZ,\nvagy Cancel/ESC-et a FELTÖLTÉSHEZ:\n\n', "u_just1": '\nlehet, hogy jobban működik, ha csak egy fájlt jelölsz ki', "u_ff_many": 'ha Linux / MacOS / Android rendszert használsz, ennyi fájl fagyást okozhat Firefoxban!\nha ez történik, próbáld újra (vagy használd a Chrome-ot).', "u_up_life": 'ez a feltöltés a befejezés után\n{0} múlva törlődik a szerverről', "u_asku": 'feltöltsem ezt a {0} fájlt ide: {1}?', "u_unpt": 'visszavonhatod/törölheted a feltöltést a bal felső 🧯 gombbal', "u_bigtab": '{0} fájl listázása következik\n\nez lefagyaszthatja a böngészőt, biztos?', "u_scan": 'Fájlok beolvasása...', "u_dirstuck": 'a rendszer elakadt az alábbi {0} elemnél; kihagyjuk őket:', "u_etadone": 'Kész ({0}, {1} fájl)', "u_etaprep": '(előkészítés...)', "u_hashdone": 'hashelés kész', "u_hashing": 'hash', "u_hs": 'kapcsolódás...', "u_started": 'a feltöltés elindult; lásd [🚀]', "u_dupdefer": 'másolat; a végén fogjuk feldolgozni', "u_actx": 'kattints ide, hogy a böngésző ne fogja vissza a sebességet,\nha más ablakba/fülre mész', "u_fixed": 'Oké! Javítva 👍', "u_cuerr": 'hiba a(z) {0} / {1}. egység feltöltésekor;\nvalószínűleg nem gond, folytatjuk\n\nfájl: {2}', "u_cuerr2": 'szerver elutasította az egységet ({0} / {1});\nkésőbb újrapróbáljuk\n\nfájl: {2}\n\nhiba ', "u_ehstmp": 'újrapróbálás; lásd jobb lent', "u_ehsfin": 'szerver elutasította a befejezést; újrapróbáljuk...', "u_ehssrch": 'szerver elutasította a keresést; újrapróbáljuk...', "u_ehsinit": 'szerver elutasította a kezdést; újrapróbáljuk...', "u_eneths": 'hálózati hiba a kapcsolódáskor; újrapróbálás...', "u_enethd": 'hálózati hiba az ellenőrzéskor; újrapróbálás...', "u_cbusy": 'várakozás, hogy a szerver megint bízzon bennünk hiba után...', "u_ehsdf": 'a szerveren elfogyott a hely!\n\naddig próbálkozunk, amíg valaki\nfel nem szabadít egy kis területet', "u_emtleak1": 'úgy tűnik, a böngésződ memóriát szivárogtat;\nkérlek', "u_emtleak2": ' válts https-re (ajánlott) vagy ', "u_emtleak3": ' ', "u_emtleakc": 'próbáld a következőt:\n
          • nyomj F5-öt
          • kapcsold ki az  mt  gombot a  ⚙️ beállításokban
          • majd próbáld újra
          Lassabb lesz, de legalább lemegy.\nBocsi a kellemetlenségért!\n\nPS: a chrome v107 már javított ezen', "u_emtleakf": 'próbáld a következőt:\n
          • nyomj F5-öt
          • kapcsold be az egyszerű módot
          • majd próbáld újra megint
          \nPS: remélhetőleg a firefox is javítja egyszer', "u_s404": 'nincs a szerveren', "u_expl": 'magyarázat', "u_maxconn": 'a legtöbb böngésző limitje 6, de Firefoxban az about:config alatt a connections-per-server értékkel növelhető', "u_tu": '

          FIGYELEM: turbo bekapcsolva,  lehet, hogy nem veszi észre a félbeszakadt feltöltéseket; lásd a súgót

          ', "u_ts": '

          FIGYELEM: turbo bekapcsolva,  a keresési eredmények hibásak lehetnek; lásd a súgót

          ', "u_turbo_c": 'turbo letiltva a szerveren', "u_turbo_g": 'turbo kikapcsolva, mert nincs jogod mappalistázáshoz ezen a köteten', "u_life_cfg": 'autotörlés perc (vagy óra) múlva', "u_life_est": 'a feltöltés törlődni fog ekkor: ---', "u_life_max": 'ez a mappa kényszeríti a\nmax élettartamot: {0}', "u_unp_ok": 'unpost engedélyezve eddig: {0}', "u_unp_ng": 'unpost NEM lesz engedélyezve', "ue_ro": 'ebben a mappában csak olvasási jogod van\n\n', "ue_nl": 'nem vagy belépve', "ue_la": 'jelenleg mint “{0}” vagy bejelentkezve', "ue_sr": 'kereső módban vagy\n\nválts feltöltésre a nagy KERESÉS gomb melletti nagyítóra 🔎 kattintva,\nés próbáld újra\n\nbocsi', "ue_ta": 'próbáld meg újra, most már mennie kell', "ue_ab": 'ez a fájl már fel van töltve egy másik mappába, várd meg, amíg az kész lesz.\n\nmegszakíthatod a korábbi próbálkozást a bal felső 🧯 gombbal', "ur_1uo": 'OK: Fájl sikeresen feltöltve', "ur_auo": 'OK: Mind a(z) {0} fájl feltöltve', "ur_1so": 'OK: Fájl megvan a szerveren', "ur_aso": 'OK: Mind a(z) {0} fájl megvan a szerveren', "ur_1un": 'A feltöltés nem sikerült', "ur_aun": 'Az összes ({0}) feltöltés megszakadt', "ur_1sn": 'Fájl nincs a szerveren', "ur_asn": 'A(z) {0} fájl nincs a szerveren', "ur_um": 'Kész;\n{0} sikeres,\n{1} sajnos nem sikerült', "ur_sm": 'Kész;\n{0} megvan a szerveren,\n{1} nincs meg', "rc_opn": 'megnyitás', "rc_ply": 'lejátszás', "rc_pla": 'lejátszás audióként', "rc_txt": 'megnyitás szövegként', "rc_md": 'megnyitás markdownként', "rc_dl": 'letöltés', "rc_zip": 'letöltés archívumként', "rc_cpl": 'link másolása', "rc_del": 'törlés', "rc_cut": 'kivágás', "rc_cpy": 'másolás', "rc_pst": 'beillesztés', "rc_rnm": 'átnevezés', "rc_nfo": 'új mappa', "rc_nfi": 'új fájl', "rc_sal": 'összes kijelölése', "rc_sin": 'kijelölés megfordítása', "rc_shf": 'mappa megosztása', "rc_shs": 'kijelöltek megosztása', "lang_set": 'frissítsünk, hogy átálljon a nyelv?', "splash": { "a1": 'frissítés', "b1": 'üdv idegen   (nem vagy bejelentkezve)', "c1": 'kilépés', "d1": 'dump stack', "d2": 'minden aktív szál állapotának mutatása', "e1": 'config újratöltés', "e2": 'konfigurációs fájlok (fiókok/kötetek) és e2ds kötetek újraszkennelése$N$Nmegjegyzés: a globális beállításokhoz$Nteljes újraindítás kell', "f1": 'böngészhető:', "g1": 'ide tudsz feltölteni:', "cc1": 'egyéb dolgok:', "h1": 'k304 kikapcsolása', "i1": 'k304 bekapcsolása', "j1": 'a k304 lekapcsolja a klienst minden HTTP 304-nél, ami segíthet némely akadózó proxynál, de amúgy lassítja a dolgokat', "k1": 'beállítások alaphelyzetbe állítása', "l1": 'jelentkezz be a többiért:', "ls3": 'belépés', "lu4": 'felhasználónév', "lp4": 'jelszó', "lo3": '“{0}” kijelentkeztetése mindenhonnan', "lo2": 'minden böngészőben lelövi a munkamenetet', "m1": 'üdv újra,', "n1": '404 nincs meg  ┐( ´ -`)┌', "o1": 'vagy nincs jogod -- próbálj belépni vagy menj a főoldalra', "p1": '403 tiltva  ~┻━┻', "q1": 'kell egy jelszó vagy menj a főoldalra', "r1": 'vissza a főoldalra', ".s1": 'újraszkennelés', "t1": 'művelet', "u2": 'utolsó írás óta eltelt idő$N( feltöltés / átnevezés / ... )$N$N17n = 17 nap$N1ó23 = 1 óra 23 perc$N4p56 = 4 perc 56 másodperc', "v1": 'csatlakozás', "v2": 'szerver használata helyi meghajtóként', "w1": 'váltás https-re', "x1": 'jelszó megváltoztatása', "y1": 'megosztások szerkesztése', "z1": 'megosztás feloldása:', "ta1": 'írd be az új jelszavad', "ta2": 'új jelszó még egyszer:', "ta3": 'elírtad; próbáld újra', "nop": 'HIBA: A jelszó nem lehet üres', "nou": 'HIBA: A felhasználónév és jelszó nem lehet üres', "aa1": 'érkező fájlok:', "ab1": 'no304 kikapcsolása', "ac1": 'no304 bekapcsolása', "ad1": 'a no304 kikapcsolja a gyorsítótárazást; akkor használd, ha a k304 nem segített. Figyelem: ez jelentős adatforgalmat generál!', "ae1": 'futó letöltések:', "af1": 'utóbbi feltöltések mutatása', "ag1": 'idp cache megtekintése', }, }; ================================================ FILE: copyparty/web/tl/ita.js ================================================ // Le righe che terminano con //m sono traduzioni automatiche non verificate Ls.ita = { "tt": "Italiano", "cols": { "c": "pulsanti azione", "dur": "durata", "q": "qualità / bitrate", "Ac": "codec audio", "Vc": "codec video", "Fmt": "formato / container", "Ahash": "checksum audio", "Vhash": "checksum video", "Res": "risoluzione", "T": "tipo file", "aq": "qualità audio / bitrate", "vq": "qualità video / bitrate", "pixfmt": "subsampling / struttura pixel", "resw": "risoluzione orizzontale", "resh": "risoluzione verticale", "chs": "canali audio", "hz": "frequenza di campionamento", }, "hks": [ [ "varie", ["ESC", "chiudi vari elementi"], "file-manager", ["G", "alterna vista lista / griglia"], ["T", "alterna miniature / icone"], ["⇧ A/D", "dimensione miniature"], ["ctrl-K", "elimina selezionati"], ["ctrl-X", "taglia selezione negli appunti"], ["ctrl-C", "copia selezione negli appunti"], ["ctrl-V", "incolla (sposta/copia) qui"], ["Y", "scarica selezionati"], ["F2", "rinomina selezionati"], "file-list-sel", ["spazio", "alterna selezione file"], ["↑/↓", "sposta cursore selezione"], ["ctrl ↑/↓", "sposta cursore e viewport"], ["⇧ ↑/↓", "seleziona file prec/succ"], ["ctrl-A", "seleziona tutti i file / cartelle"], ], [ "navigation", ["B", "alterna breadcrumb / pannello nav"], ["I/K", "cartella prec/succ"], ["M", "cartella genitore (o comprimi corrente)"], ["V", "alterna cartelle / file di testo nel pannello nav"], ["A/D", "dimensione pannello nav"], ], [ "audio-player", ["J/L", "brano prec/succ"], ["U/O", "salta 10sec indietro/avanti"], ["0..9", "salta a 0%..90%"], ["P", "play/pausa (avvia anche)"], ["S", "seleziona brano in riproduzione"], ["Y", "scarica brano"], ], [ "image-viewer", ["J/L, ←/→", "immagine prec/succ"], ["Home/End", "prima/ultima immagine"], ["F", "schermo intero"], ["R", "ruota in senso orario"], ["⇧ R", "ruota in senso antiorario"], ["S", "seleziona immagine"], ["Y", "scarica immagine"], ], [ "video.player", ["U/O", "salta 10sec indietro/avanti"], ["P/K/Spazio", "play/pausa"], ["C", "continua riproduzione successivo"], ["V", "loop"], ["M", "muto"], ["[ e ]", "imposta intervallo loop"], ], [ "textfile-viewer", ["I/K", "file prec/succ"], ["M", "chiudi file di testo"], ["E", "modifica file di testo"], ["S", "seleziona file (per taglia/copia/rinomina)"], ["Y", "scarica il file di testo"], //m ["⇧ J", "abbellire json"], //m ] ], "m_ok": "OK", "m_ng": "Annulla", "enable": "Abilita", "danger": "PERICOLO", "clipped": "copiato negli appunti", "ht_s1": "secondo", "ht_s2": "secondi", "ht_m1": "minuto", "ht_m2": "minuti", "ht_h1": "ora", "ht_h2": "ore", "ht_d1": "giorno", "ht_d2": "giorni", "ht_and": " e ", "goh": "control-panel", "gop": 'cartella sorella precedente">prec', "gou": 'cartella genitore">su', "gon": 'prossima cartella">succ', "logout": "Logout ", "login": "Accedi", //m "access": " accesso", "ot_close": "chiudi sottomenu", "ot_search": "`cerca file per attributi, percorso / nome, tag musicali, o qualsiasi combinazione di questi$N$N`foo bar` = deve contenere sia «foo» che «bar»,$N`foo -bar` = deve contenere «foo» ma non «bar»,$N`^yana .opus$` = inizia con «yana» ed è un file «opus»$N`"try unite"` = contiene esattamente «try unite»$N$Nil formato data è iso-8601, come$N`2009-12-31` o `2020-09-12 23:30:00`", "ot_unpost": "unpost: elimina i tuoi caricamenti recenti, o interrompi quelli non completati", "ot_bup": "bup: uploader di base, supporta anche netscape 4.0", "ot_mkdir": "mkdir: crea una nuova directory", "ot_md": "new-file: crea un nuovo file di testo", //m "ot_msg": "msg: invia un messaggio al log del server", "ot_mp": "opzioni lettore multimediale", "ot_cfg": "opzioni di configurazione", "ot_u2i": 'up2k: carica file (se hai accesso in scrittura) o attiva la modalità ricerca per vedere se esistono già da qualche parte sul server$N$NI caricamenti sono ripristinabili, multithreaded, e i timestamp dei file vengono preservati, ma usa più CPU di [🎈]  (l\'uploader di base)

          durante i caricamenti, questa icona diventa un indicatore di progresso!', "ot_u2w": 'up2k: carica file con supporto per il ripristino (chiudi il browser e trascina gli stessi file più tardi)$N$NMultithreaded, e i timestamp dei file vengono preservati, ma usa più CPU di [🎈]  (l\'uploader di base)

          durante i caricamenti, questa icona diventa un indicatore di progresso!', "ot_noie": 'Perfavore usa Chrome / Firefox / Edge', "ab_mkdir": "crea directory", "ab_mkdoc": "nuovo file di testo", //m "ab_msg": "invia msg al log srv", "ay_path": "salta alle cartelle", "ay_files": "salta ai file", "wt_ren": "rinomina elementi selezionati$NTasto rapido: F2", "wt_del": "elimina elementi selezionati$NTasto rapido: ctrl-K", "wt_cut": "taglia elementi selezionati <small>(poi incolla altrove)</small>$NTasto rapido: ctrl-X", "wt_cpy": "copia elementi selezionati negli appunti$N(per incollarli altrove)$NTasto rapido: ctrl-C", "wt_pst": "incolla una selezione precedentemente tagliata / copiata$NTasto rapido: ctrl-V", "wt_selall": "seleziona tutti i file$NTasto rapido: ctrl-A (quando il file è focalizzato)", "wt_selinv": "inverti selezione", "wt_zip1": "scarica questa cartella come archivio", "wt_selzip": "scarica selezione come archivio", "wt_seldl": "scarica selezione come file separati$NTasto rapido: Y", "wt_npirc": "copia info traccia formato irc", "wt_nptxt": "copia info traccia testo semplice", "wt_m3ua": "aggiungi alla playlist m3u (clicca 📻copia dopo)", "wt_m3uc": "copia playlist m3u negli appunti", "wt_grid": "alterna vista griglia / lista$NTasto rapido: G", "wt_prev": "traccia precedente$NTasto rapido: J", "wt_play": "play / pausa$NTasto rapido: P", "wt_next": "traccia successiva$NTasto rapido: L", "ul_par": "caricamenti paralleli:", "ut_rand": "randomizza nomi file", "ut_u2ts": "copia il timestamp di ultima modifica$Ndal tuo filesystem al server\">📅", "ut_ow": "sovrascrivere file esistenti sul server?$N🛡️: mai (genererà un nuovo nome file)$N🕒: sovrascrivi se il file del server è più vecchio del tuo$N♻️: sovrascrivi sempre se i file sono diversi$N⏭️: ignora sempre tutti i file esistenti", //m "ut_mt": "continua l'hashing di altri file durante il caricamento$N$NProva a disabilitare se la tua CPU o HDD è un collo di bottiglia", "ut_ask": 'chiedi conferma prima che inizi il caricamento">💭', "ut_pot": "migliora la velocità di caricamento su dispositivi lenti$Nrendendo l'interfaccia meno complessa", "ut_srch": "non caricare realmente, invece controlla se i file esistono già $N sul server (scansionerà tutte le cartelle che puoi leggere)", "ut_par": "metti in pausa i caricamenti impostandolo a 0$N$NAumenta se la tua connessione è lenta / alta latenza$N$NMantienilo a 1 su LAN o se l'HDD del server è un collo di bottiglia", "ul_btn": "trascina file / cartelle
          qui (o cliccami)", "ul_btnu": "C A R I C A", "ul_btns": "C E R C A", "ul_hash": "hash", "ul_send": "invia", "ul_done": "fatto", "ul_idle1": "nessun caricamento ancora in coda", "ut_etah": "velocità media di <em>hashing</em>, e tempo stimato al completamento", "ut_etau": "velocità media di <em>caricamento</em> e tempo stimato al completamento", "ut_etat": "velocità <em>totale</em> media e tempo stimato al completamento", "uct_ok": "completato con successo", "uct_ng": "non-valido: fallito / rifiutato / non-trovato", "uct_done": "ok e ng combinati", "uct_bz": "hashing o caricamento", "uct_q": "inattivo, in attesa", "utl_name": "nome file", "utl_ulist": "lista", "utl_ucopy": "copia", "utl_links": "link", "utl_stat": "stato", "utl_prog": "progresso", // keep short: "utl_404": "404", "utl_err": "ERRORE", "utl_oserr": "Errore-SO", "utl_found": "trovato", "utl_defer": "rinvia", "utl_yolo": "YOLO", "utl_done": "finito", "ul_flagblk": "i file sono stati aggiunti alla coda
          tuttavia c'è un up2k occupato in un'altra scheda del browser,
          quindi aspetto che quello finisca prima", "ul_btnlk": "la configurazione del server ha bloccato questo interruttore in questo stato", "udt_up": "Carica", "udt_srch": "Cerca", "udt_drop": "lascialo qui", "u_nav_m": '
          ok, cosa hai?
          Invio = File (uno o più)\nESC = Una cartella (incluse sottocartelle)', "u_nav_b": 'FileUna cartella', "cl_opts": "opzioni", "cl_hfsz": "dimensione file", //m "cl_themes": "tema", "cl_langs": "lingua", "cl_ziptype": "download cartella", "cl_uopts": "opzioni up2k", "cl_favico": "favicon", "cl_bigdir": "cartelle grandi", "cl_hsort": "#ordinamento", "cl_keytype": "notazione tasti", "cl_hiddenc": "colonne nascoste", "cl_hidec": "nascondi", "cl_reset": "reset", "cl_hpick": "tocca le intestazioni delle colonne per nascondere nella tabella sottostante", "cl_hcancel": "nascondere colonne annullato", "cl_rcm": "menu contestuale", //m "ct_grid": '田 griglia', "ct_ttips": '◔ ◡ ◔">ℹ️ tooltip', "ct_thumb": 'nella vista griglia, alterna icone o miniature$NTasto rapido: T">🖼️ miniature', "ct_csel": 'usa CTRL e SHIFT per la selezione file nella vista griglia">sel', "ct_dsel": 'usa la selezione tramite trascinamento nella vista griglia">trascina', //m "ct_dl": 'forza il download (non visualizzare inline) quando si clicca su un file">dl', //m "ct_ihop": 'quando il visualizzatore immagini è chiuso, scorri fino all\'ultimo file visualizzato">g⮯', "ct_dots": 'mostra file nascosti (se il server lo permette)">dotfile', "ct_qdel": 'quando elimini file, chiedi conferma solo una volta">qdel', "ct_dir1st": 'ordina cartelle prima dei file">📁 prima', "ct_nsort": 'ordinamento naturale (per nomi file con cifre iniziali)">nsort', "ct_utc": 'mostra tutte le date/ore in UTC">UTC', "ct_readme": 'mostra README.md negli elenchi cartelle">📜 readme', "ct_idxh": 'mostra index.html invece dell\'elenco cartelle">htm', "ct_sbars": 'mostra barre di scorrimento">⟊', "cut_umod": "se un file esiste già sul server, aggiorna il timestamp di ultima modifica del server per farlo coincidere con il tuo file locale (richiede permessi di scrittura+eliminazione)\">re📅", "cut_turbo": "il pulsante yolo, probabilmente NON lo vuoi abilitare:$N$NUsalo se stavi caricando una grande quantità di file e hai dovuto riavviare per qualche motivo, e vuoi continuare il caricamento il prima possibile$N$NQuesto sostituisce il controllo hash con un semplice "questo ha la stessa dimensione file sul server?" quindi se il contenuto del file è diverso NON verrà caricato$N$NDovresti spegnere questo quando il caricamento è finito, e poi "caricare" di nuovo gli stessi file per far verificare al client\">turbo", "cut_datechk": "non ha effetto a meno che il pulsante turbo sia abilitato$N$NRiduce il fattore yolo di una piccola quantità; controlla se i timestamp dei file sul server corrispondono ai tuoi$N$NDovrebbe teoricamente catturare la maggior parte dei caricamenti non finiti / corrotti, ma non è un sostituto per fare un passaggio di verifica con turbo disabilitato dopo\">date-chk", "cut_u2sz": "dimensione (in MiB) di ogni chunk di caricamento; valori grandi volano meglio attraverso l'atlantico. Prova valori bassi su connessioni molto inaffidabili", "cut_flag": "assicura che solo una scheda stia caricando alla volta $N -- anche le altre schede devono avere questo abilitato $N -- influisce solo sulle schede dello stesso dominio", "cut_az": "carica file in ordine alfabetico, invece che dal file più piccolo prima$N$NL'ordine alfabetico può rendere più facile controllare a occhio se qualcosa è andato storto sul server, ma rende il caricamento leggermente più lento su fibra / LAN", "cut_nag": "notifica SO quando il caricamento si completa$N(solo se il browser o la scheda non è attiva)", "cut_sfx": "allarme sonoro quando il caricamento si completa$N(solo se il browser o la scheda non è attiva)", "cut_mt": "usa multithreading per accelerare l'hashing dei file$N$NQuesto usa web-worker e richiede$Npiù RAM (fino a 512 MiB extra)$N$NRende https 30% più veloce, http 4.5x più veloce\">mt", "cut_wasm": "usa wasm invece dell'hasher integrato del browser; migliora la velocità sui browser basati su chrome ma aumenta il carico CPU, e molte versioni vecchie di chrome hanno bug che fanno consumare tutta la RAM al browser e crashare se questo è abilitato\">wasm", "cft_text": "testo favicon (vuoto e aggiorna per disabilitare)", "cft_fg": "colore primo piano", "cft_bg": "colore sfondo", "cdt_lim": "numero massimo di file da mostrare in una cartella", "cdt_ask": "quando scorri verso il fondo,$Ninvece di caricare più file,$Nchiedi cosa fare", "cdt_hsort": "`quante regole di ordinamento (`,sorthref`) includere negli URL multimediali. Impostandolo a 0 ignorerà anche le regole di ordinamento incluse nei link multimediali quando li clicchi", "cdt_ren": "abilita il menu contestuale personalizzato, il menu normale è accessibile con shift + clic destro\">abilita", //m "cdt_rdb": "mostra il menu normale con il tasto destro quando quello personalizzato è già aperto e si clicca di nuovo\">x2", //m "tt_entree": "mostra pannello nav (barra laterale albero directory)$NTasto rapido: B", "tt_detree": "mostra breadcrumb$NTasto rapido: B", "tt_visdir": "scorri alla cartella selezionata", "tt_ftree": "alterna albero cartelle / file di testo$NTasto rapido: V", "tt_pdock": "mostra cartelle genitore in un pannello ancorato in alto", "tt_dynt": "crescita automatica mentre l'albero si espande", "tt_wrap": "a capo parola", "tt_hover": "rivela righe che traboccano al passaggio del mouse$N( interrompe lo scorrimento a meno che il cursore $N  del mouse non sia nella grondaia sinistra )", "ml_pmode": "alla fine della cartella...", "ml_btns": "comandi", "ml_tcode": "transcodifica", "ml_tcode2": "transcodifica in", "ml_tint": "tinta", "ml_eq": "equalizzatore audio", "ml_drc": "compressore gamma dinamica", "ml_ss": "salta i silenzi", //m "mt_loop": "loop/ripeti una canzone\">🔁", "mt_one": "fermati dopo una canzone\">1️⃣", "mt_shuf": "mescola le canzoni in ogni cartella\">🔀", "mt_aplay": "autoplay se c'è un song-ID nel link che hai cliccato per accedere al server$N$NDisabilitando questo fermerà anche l'aggiornamento dell'URL della pagina con song-ID quando riproduci musica, per prevenire autoplay se queste impostazioni vengono perse ma l'URL rimane\">a▶", "mt_preload": "inizia a caricare la prossima canzone verso la fine per riproduzione senza interruzioni\">preload", "mt_prescan": "vai alla prossima cartella prima che finisca l'ultima canzone$Nmantenendo felice il browser web$Ncosì non si ferma la riproduzione\">nav", "mt_fullpre": "prova a precaricare l'intera canzone;$N✅ abilita su connessioni inaffidabili,$N❌ disabilita su connessioni lente probabilmente\">full", "mt_fau": "sui telefoni, previeni che la musica si fermi se la prossima canzone non si precarica abbastanza velocemente (può rendere glitchy la visualizzazione dei tag)\">☕️", "mt_waves": "barra di ricerca forma d'onda:$Nmostra ampiezza audio nello scrubber\">~s", "mt_npclip": "mostra pulsanti per copiare negli appunti la canzone attualmente in riproduzione\">/np", "mt_m3u_c": "mostra pulsanti per copiare negli appunti le$Ncanzoni selezionate come voci playlist m3u8\">📻", "mt_octl": "integrazione so (tasti multimediali / osd)\">os-ctl", "mt_oseek": "permetti ricerca attraverso integrazione so$N$Nnota: su alcuni dispositivi (iPhone),$Nquesto sostituisce il pulsante canzone successiva\">seek", "mt_oscv": "mostra copertina album in osd\">art", "mt_follow": "mantieni la traccia in riproduzione scorrevole nella vista\">🎯", "mt_compact": "controlli compatti\">⟎", "mt_uncache": "pulisci cache  (prova ad attivare se il tuo browser ha messo in cache$Nuna copia rotta di una canzone e si rifiuta di riprodurla)\">uncache", "mt_mloop": "loop della cartella aperta\">🔁 loop", "mt_mnext": "carica la prossima cartella e continua\">📂 succ", "mt_mstop": "ferma riproduzione\">⏸ stop", "mt_cflac": "converti flac / wav in {0}\">flac", "mt_caac": "converti aac / m4a in {0}\">aac", "mt_coth": "converti tutti gli altri (non mp3) in {0}\">oth", "mt_c2opus": "scelta migliore per desktop, laptop, android\">opus", "mt_c2owa": "opus-weba, per iOS 17.5 e più recenti\">owa", "mt_c2caf": "opus-caf, per iOS 11 fino a 17\">caf", "mt_c2mp3": "usa questo su dispositivi molto vecchi\">mp3", "mt_c2flac": "qualità audio migliore, ma download pesanti\">flac", //m "mt_c2wav": "riproduzione non compressa (ancora più grande)\">wav", //m "mt_c2ok": "bene, buona scelta", "mt_c2nd": "quello non è il formato di output raccomandato per il tuo dispositivo, ma va bene", "mt_c2ng": "il tuo dispositivo non sembra supportare questo formato di output, ma proviamo comunque", "mt_xowa": "ci sono bug in iOS che prevengono la riproduzione in background usando questo formato; usa caf o mp3 invece", "mt_tint": "livello sfondo (0-100) sulla barra di ricerca$Nper rendere il buffering meno distraente", "mt_eq": "`abilita l'equalizzatore e controllo guadagno;$N$Nboost `0` = volume standard 100% (non modificato)$N$Nwidth `1  ` = stereo standard (non modificato)$Nwidth `0.5` = 50% crossfeed sinistra-destra$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = rimozione vocale :^)$N$Nabilitando l'equalizzatore rende gli album senza interruzioni completamente senza interruzioni, quindi lascialo acceso con tutti i valori a zero (eccetto width = 1) se ti importa di quello", "mt_drc": "abilita il compressore gamma dinamica (appiattitore volume / brickwaller); abiliterà anche EQ per bilanciare gli spaghetti, quindi imposta tutti i campi EQ eccetto 'width' a 0 se non lo vuoi$N$NAbbassa il volume dell'audio sopra THRESHOLD dB; per ogni RATIO dB oltre THRESHOLD c'è 1 dB di output, quindi i valori di default di tresh -24 e ratio 12 significa che non dovrebbe mai diventare più forte di -22 dB ed è sicuro aumentare il boost equalizzatore a 0.8, o anche 1.8 con ATK 0 e un RLS enorme come 90 (funziona solo in firefox; RLS è max 1 in altri browser)$N$N(vedi wikipedia, lo spiegano molto meglio)", "mt_ss": "`abilita il salto del silenzio; moltiplica la velocità di riproduzione per `av` vicino a inizio/fine quando il volume è sotto `vol` e la posizione è nei primi `ini`% o ultimi `fin`%", //m "mt_ssvt": "soglia volume (0-255)\">vol", //m "mt_ssts": "soglia attiva (% traccia, inizio)\">ini", //m "mt_sste": "soglia attiva (% traccia, fine)\">fin", //m "mt_sssm": "moltiplicatore velocità riproduzione\">av", //m "mb_play": "riproduci", "mm_hashplay": "riprodurre questo file audio?", "mm_m3u": "premi Invio/OK per Riprodurre\npremi ESC/Annulla per Modificare", "mp_breq": "serve firefox 82+ o chrome 73+ o iOS 15+", "mm_bload": "ora caricando...", "mm_bconv": "convertendo in {0}, attendi...", "mm_opusen": "il tuo browser non può riprodurre file aac / m4a;\ntranscodifica in opus ora abilitata", "mm_playerr": "riproduzione fallita: ", "mm_eabrt": "Il tentativo di riproduzione è stato cancellato", "mm_enet": "La tua connessione internet è instabile", "mm_edec": "Questo file è presumibilmente corrotto??", "mm_esupp": "Il tuo browser non capisce questo formato audio", "mm_eunk": "Errore Sconosciuto", "mm_e404": "Non è stato possibile riprodurre audio; errore 404: File non trovato.", "mm_e403": "Non è stato possibile riprodurre audio; errore 403: Accesso negato.\n\nProva a premere F5 per ricaricare, forse sei stato disconnesso", "mm_e415": "Non è stato possibile riprodurre audio; errore 415: Conversione del file non riuscita; controlla i log del server.", //m "mm_e500": "Non è stato possibile riprodurre audio; errore 500: Controlla i log del server.", "mm_e5xx": "Non è stato possibile riprodurre audio; errore server ", "mm_nof": "non trovo altri file audio nelle vicinanze", "mm_prescan": "Cercando musica da riprodurre dopo...", "mm_scank": "Trovata la prossima canzone:", "mm_uncache": "cache pulita; tutte le canzoni si riscaricheranno alla prossima riproduzione", "mm_hnf": "quella canzone non esiste più", "im_hnf": "quell'immagine non esiste più", "f_empty": 'questa cartella è vuota', "f_chide": 'questo nasconderà la colonna «{0}»\n\npuoi mostrare le colonne nella scheda impostazioni', "f_bigtxt": "questo file è {0} MiB grande -- visualizzare davvero come testo?", "f_bigtxt2": "visualizzare solo la fine del file invece? questo abiliterà anche following/tailing, mostrando righe di testo appena aggiunte in tempo reale", "fbd_more": '
          mostrando {0} di {1} file; mostra {2} o mostra tutti
          ', "fbd_all": '
          mostrando {0} di {1} file; mostra tutti
          ', "f_anota": "solo {0} dei {1} elementi sono stati selezionati;\nper selezionare l'intera cartella, prima scorri fino in fondo", "f_dls": 'i link dei file nella cartella corrente sono stati\ncambiati in link di download', "f_dl_nd": 'cartella ignorata (usa invece il download zip/tar):\n', //m "f_partial": "Per scaricare in sicurezza un file che è attualmente in fase di caricamento, clicca il file che ha lo stesso nome, ma senza l'estensione .PARTIAL. Premi ANNULLA o Escape per farlo.\n\nPremendo OK / Invio ignorerai questo avviso e continuerai a scaricare il file .PARTIAL scratch, che quasi sicuramente ti darà dati corrotti.", "ft_paste": "incolla {0} elementi$NTasto rapido: ctrl-V", "fr_eperm": 'impossibile rinominare:\nnon hai il permesso “sposta” in questa cartella', "fd_eperm": 'impossibile eliminare:\nnon hai il permesso “elimina” in questa cartella', "fc_eperm": 'impossibile tagliare:\nnon hai il permesso “sposta” in questa cartella', "fp_eperm": 'impossibile incollare:\nnon hai il permesso “scrivi” in questa cartella', "fr_emore": "seleziona almeno un elemento da rinominare", "fd_emore": "seleziona almeno un elemento da eliminare", "fc_emore": "seleziona almeno un elemento da tagliare", "fcp_emore": "seleziona almeno un elemento da copiare negli appunti", "fs_sc": "condividi la cartella in cui ti trovi", "fs_ss": "condividi i file selezionati", "fs_just1d": "non puoi selezionare più di una cartella,\no mescolare file e cartelle in una selezione", "fs_abrt": "❌ interrompi", "fs_rand": "🎲 nome.casuale", "fs_go": "✅ crea condivisione", "fs_name": "nome", "fs_src": "sorgente", "fs_pwd": "password", "fs_exp": "scadenza", "fs_tmin": "min", "fs_thrs": "ore", "fs_tdays": "giorni", "fs_never": "eterno", "fs_pname": "nome link opzionale; sarà casuale se vuoto", "fs_tsrc": "il file o cartella da condividere", "fs_ppwd": "password opzionale", "fs_w8": "creando condivisione...", "fs_ok": "premi Invio/OK per Appunti\npremi ESC/Annulla per Chiudere", "frt_dec": "può risolvere alcuni casi di nomi file corrotti\">url-decode", "frt_rst": "ripristina nomi file modificati a quelli originali\">↺ reset", "frt_abrt": "interrompi e chiudi questa finestra\">❌ annulla", "frb_apply": "APPLICA RINOMINA", "fr_adv": "rinomina batch / metadata / pattern\">avanzato", "fr_case": "regex case-sensitive\">maiusc", "fr_win": "nomi sicuri per windows; sostituisce <>:"\\|?* con caratteri giapponesi fullwidth\">win", "fr_slash": "sostituisce / con un carattere che non causa la creazione di nuove cartelle\">no /", "fr_re": "`pattern di ricerca regex da applicare ai nomi file originali; i gruppi di cattura possono essere referenziati nel campo formato sottostante come `(1)` e `(2)` e così via", "fr_fmt": "`ispirato da foobar2000:$N`(title)` è sostituito dal titolo della canzone,$N`[(artist) - ](title)` salta [questa] parte se artista è vuoto$N`$lpad((tn),2,0)` aggiunge padding al numero traccia a 2 cifre", "fr_pdel": "elimina", "fr_pnew": "salva come", "fr_pname": "fornisci un nome per il tuo nuovo preset", "fr_aborted": "interrotto", "fr_lold": "nome vecchio", "fr_lnew": "nome nuovo", "fr_tags": "tag per i file selezionati (sola lettura, solo per riferimento):", "fr_busy": "rinominando {0} elementi...\n\n{1}", "fr_efail": "rinomina fallita:\n", "fr_nchg": "{0} dei nuovi nomi sono stati alterati a causa di win e/o no /\n\nOK per continuare con questi nuovi nomi alterati?", "fd_ok": "eliminazione OK", "fd_err": "eliminazione fallita:\n", "fd_none": "niente è stato eliminato; forse bloccato dalla configurazione server (xbd)?", "fd_busy": "eliminando {0} elementi...\n\n{1}", "fd_warn1": "ELIMINARE questi {0} elementi?", "fd_warn2": "Ultima possibilità! Nessun modo per annullare. Eliminare?", "fc_ok": "tagliati {0} elementi", "fc_warn": 'tagliati {0} elementi\n\nma: solo questa scheda-browser può incollarli\n(dato che la selezione è così assolutamente massiva)', "fcc_ok": "copiati {0} elementi negli appunti", "fcc_warn": 'copiati {0} elementi negli appunti\n\nma: solo questa scheda-browser può incollarli\n(dato che la selezione è così assolutamente massiva)', "fp_apply": "usa questi nomi", "fp_skip": "salta conflitti", //m "fp_ecut": "prima taglia o copia alcuni file / cartelle da incollare / spostare\n\nnota: puoi tagliare / incollare attraverso diverse schede del browser", "fp_ename": "{0} elementi non possono essere spostati qui perché i nomi sono già presi. Dai loro nuovi nomi qui sotto per continuare, o lascia vuoto il nome (\"salta conflitti\") per saltarli:", //m "fcp_ename": "{0} elementi non possono essere copiati qui perché i nomi sono già presi. Dai loro nuovi nomi qui sotto per continuare, o lascia vuoto il nome (\"salta conflitti\") per saltarli:", //m "fp_emore": "ci sono ancora alcune collisioni di nomi file rimaste da risolvere", "fp_ok": "spostamento OK", "fcp_ok": "copia OK", "fp_busy": "spostando {0} elementi...\n\n{1}", "fcp_busy": "copiando {0} elementi...\n\n{1}", "fp_abrt": "annullamento in corso...", //m "fp_err": "spostamento fallito:\n", "fcp_err": "copia fallita:\n", "fp_confirm": "spostare questi {0} elementi qui?", "fcp_confirm": "copiare questi {0} elementi qui?", "fp_etab": 'fallito leggere appunti da altra scheda browser', "fp_name": "caricando un file dal tuo dispositivo. Dagli un nome:", "fp_both_m": '
          scegli cosa incollare
          Invio = Sposta {0} file da «{1}»\nESC = Carica {2} file dal tuo dispositivo', "fcp_both_m": '
          scegli cosa incollare
          Invio = Copia {0} file da «{1}»\nESC = Carica {2} file dal tuo dispositivo', "fp_both_b": 'SpostaCarica', "fcp_both_b": 'CopiaCarica', "mk_noname": "scrivi un nome nel campo di testo a sinistra prima di farlo :p", "nmd_i1": "puoi anche aggiungere l’estensione che vuoi, per esempio .md", //m "nmd_i2": "puoi creare solo file .{0} perché non hai il permesso di eliminare", //m "tv_load": "Caricando documento di testo:\n\n{0}\n\n{1}% ({2} di {3} MiB caricati)", "tv_xe1": "impossibile caricare file di testo:\n\nerrore ", "tv_xe2": "404, file non trovato", "tv_lst": "lista di file di testo in", "tvt_close": "torna alla vista cartella$NTasto rapido: M (o Esc)\">❌ chiudi", "tvt_dl": "scarica questo file$NTasto rapido: Y\">💾 scarica", "tvt_prev": "mostra documento precedente$NTasto rapido: i\">⬆ prec", "tvt_next": "mostra documento successivo$NTasto rapido: K\">⬇ succ", "tvt_sel": "seleziona file   ( per taglia / copia / elimina / ... )$NTasto rapido: S\">sel", "tvt_j": "abbellire json$NTasto rapido: shift-J\">j", //m "tvt_edit": "apri file nell'editor di testo$NTasto rapido: E\">✏️ modifica", "tvt_tail": "monitora file per cambiamenti; mostra nuove righe in tempo reale\">📡 segui", "tvt_wrap": "a capo parola\">↵", "tvt_atail": "blocca scorrimento in fondo alla pagina\">⚓", "tvt_ctail": "decodifica colori terminale (codici escape ansi)\">🌈", "tvt_ntail": "limite scrollback (quanti byte di testo mantenere caricati)", "m3u_add1": "canzone aggiunta alla playlist m3u", "m3u_addn": "{0} canzoni aggiunte alla playlist m3u", "m3u_clip": "playlist m3u ora copiata negli appunti\n\ndovresti creare un nuovo file di testo chiamato qualcosa.m3u e incollare la playlist in quel documento; questo la renderà riproducibile", "gt_vau": "non mostrare video, riproduci solo l'audio\">🎧", "gt_msel": "abilita selezione file; ctrl-click un file per sovrascrivere$N$N<em>quando attivo: doppio-click un file / cartella per aprirlo</em>$N$NTasto rapido: S\">multiselezione", "gt_crop": "ritaglia miniature al centro\">ritaglia", "gt_3x": "miniature hi-res\">3x", "gt_zoom": "zoom", "gt_chop": "taglia", "gt_sort": "ordina per", "gt_name": "nome", "gt_sz": "dimensione", "gt_ts": "data", "gt_ext": "tipo", "gt_c1": "tronca nomi file di più (mostra meno)", "gt_c2": "tronca nomi file di meno (mostra di più)", "sm_w8": "cercando...", "sm_prev": "i risultati di ricerca qui sotto sono da una query precedente:\n ", "sl_close": "chiudi risultati ricerca", "sl_hits": "mostrando {0} risultati", "sl_moar": "carica altro", "s_sz": "dimensione", "s_dt": "data", "s_rd": "percorso", "s_fn": "nome", "s_ta": "tag", "s_ua": "car@", "s_ad": "avanz.", "s_s1": "MiB minimo", "s_s2": "MiB massimo", "s_d1": "iso8601 min.", "s_d2": "iso8601 max.", "s_u1": "caricato dopo", "s_u2": "e/o prima", "s_r1": "percorso contiene   (separato da spazi)", "s_f1": "nome contiene   (nega con -nope)", "s_t1": "tag contiene   (^=inizio, fine=$)", "s_a1": "proprietà metadata specifiche", "md_eshow": "impossibile renderizzare ", "md_off": "[📜readme] disabilitato in [⚙️] -- documento nascosto", "badreply": "Fallito nel parsare risposta dal server", "xhr403": "403: Accesso negato\n\nprova a premere F5, forse sei stato disconnesso", "xhr0": "sconosciuto (probabilmente persa connessione al server, o server offline)", "cf_ok": "scusa per quello -- la protezione DD" + wah + "oS è entrata in azione\n\nle cose dovrebbero riprendere in circa 30 sec\n\nse non succede niente, premi F5 per ricaricare la pagina", "tl_xe1": "impossibile elencare sottocartelle:\n\nerrore ", "tl_xe2": "404: Cartella non trovata", "fl_xe1": "impossibile elencare file nella cartella:\n\nerrore ", "fl_xe2": "404: Cartella non trovata", "fd_xe1": "impossibile creare sottocartella:\n\nerrore ", "fd_xe2": "404: Cartella genitore non trovata", "fsm_xe1": "impossibile inviare messaggio:\n\nerrore ", "fsm_xe2": "404: Cartella genitore non trovata", "fu_xe1": "fcaricamento fallito per la lista unpost dal server:\n\nerrore ", "fu_xe2": "404: File non trovato??", "fz_tar": "file gnu-tar non compresso (linux / mac)", "fz_pax": "tar formato pax non compresso (più lento)", "fz_targz": "gnu-tar con compressione gzip livello 3$N$NSolitamente è molto lento, quindi$Nusa tar non compresso", "fz_tarxz": "gnu-tar con compressione xz livello 1$N$NQuesto è solitamente molto lento, quindi$Nusa tar non compresso", "fz_zip8": "zip con nomi file utf8 (forse instabile su windows 7 e precedenti)", "fz_zipd": "zip con nomi file cp437 tradizionali, per software molto vecchio", "fz_zipc": "cp437 con crc32 calcolato presto,$Nper MS-DOS PKZIP v2.04g (ottobre 1993)$N(ci vuole più tempo per elaborare prima che possa iniziare il download)", "un_m1": "puoi eliminare i tuoi caricamenti recenti (o interrompere quelli non finiti) qui sotto", "un_upd": "aggiorna", "un_m4": "o condividi i file visibili qui sotto:", "un_ulist": "mostra", "un_ucopy": "copia", "un_flt": "filtro opzionale:  URL deve contenere", "un_fclr": "resetta filtro", "un_derr": 'unpost-delete fallito:\n', "un_f5": 'qualcosa si è rotto, prova un aggiornamento o premi F5', "un_uf5": "scusa ma devi aggiornare la pagina (per esempio premendo F5 o CTRL-R) prima che questo caricamento possa essere interrotto", "un_nou": 'avviso: server troppo occupato per mostrare caricamenti non finiti; clicca il link "aggiorna" tra un po\'', "un_noc": 'avviso: unpost di file completamente caricati non è abilitato/permesso nella configurazione server', "un_max": "mostrando primi 2000 file (usa il filtro)", "un_avail": "{0} caricamenti recenti possono essere eliminati
          {1} non finiti possono essere interrotti", "un_m2": "ordinati per tempo di caricamento; più recenti prima:", "un_no1": "scherzo! nessun caricamento è abbastanza recente", "un_no2": "scherzo! nessun caricamento che corrisponde a quel filtro è abbastanza recente", "un_next": "elimina i prossimi {0} file qui sotto", "un_abrt": "interrompi", "un_del": "elimina", "un_m3": "caricando i tuoi caricamenti recenti...", "un_busy": "eliminando {0} file...", "un_clip": "{0} link copiati negli appunti", "u_https1": "dovresti", "u_https2": "passare a https", "u_https3": "per prestazioni migliori", "u_ancient": 'il tuo browser è incredibilmente antico -- forse dovresti usare bup invece', "u_nowork": "serve firefox 53+ o chrome 57+ o iOS 11+", "tail_2old": "serve firefox 105+ o chrome 71+ o iOS 14.5+", "u_nodrop": 'il tuo browser è troppo vecchio per il caricamento drag-and-drop', "u_notdir": "quella non è una cartella!\n\nil tuo browser è troppo vecchio,\nprova dragdrop invece", "u_uri": "per trascinare immagini da altre finestre del browser,\nrilasciale sul pulsante upload grande", "u_enpot": 'passa alla UI patata (può migliorare velocità upload)', "u_depot": 'passa alla UI elegante (può ridurre velocità upload)', "u_gotpot": 'passando alla UI patata per migliorare velocità upload,\n\nsentiti libero di non essere d\'accordo e tornare indietro!', "u_pott": "

          file:   {0} finiti,   {1} falliti,   {2} occupati,   {3} in coda

          ", "u_ever": "questo è l'uploader di base; up2k necessita almeno
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'questo è l\'uploader di base; up2k è migliore', "u_uput": 'velocizza (salta checksum)', "u_ewrite": 'non hai accesso in scrittura a questa cartella', "u_eread": 'non hai accesso in lettura a questa cartella', "u_enoi": 'file-search non è abilitato nella configurazione server', "u_enoow": "non puoi sovrascrivere qui; serve permesso Elimina", "u_badf": 'Questi {0} file (di {1} totali) sono stati saltati, probabilmente a causa di permessi filesystem:\n\n', "u_blankf": 'Questi {0} file (di {1} totali) sono vuoti; caricarli comunque?\n\n', "u_applef": 'Questi {0} file (di {1} totali) sono probabilmente indesiderabili;\nPremi OK/Invio per SALTARE i seguenti file,\nPremi Annulla/ESC per NON escludere, e CARICARE anche quelli:\n\n', "u_just1": '\nForse funziona meglio se selezioni solo un file', "u_ff_many": "se stai usando Linux / MacOS / Android, allora questa quantità di file potrebbe far crashare Firefox!\nse succede, riprova (o usa Chrome).", "u_up_life": "Questo caricamento sarà eliminato dal server\n{0} dopo che si completa", "u_asku": 'caricare questi {0} file in {1}', "u_unpt": "puoi annullare / eliminare questo caricamento usando 🧯 in alto a sinistra", "u_bigtab": 'sto per mostrare {0} file\n\nquesto potrebbe far crashare il tuo browser, sei sicuro?', "u_scan": 'Scansionando file...', "u_dirstuck": 'iteratore directory si è bloccato tentando di accedere ai seguenti {0} elementi; salterò:', "u_etadone": 'Fatto ({0}, {1} file)', "u_etaprep": '(preparando per caricare)', "u_hashdone": 'hashing completato', "u_hashing": 'hash', "u_hs": 'handshaking...', "u_started": "i file ora sono in caricamento; vedi [🚀]", "u_dupdefer": "duplicato; sarà processato dopo tutti gli altri file", "u_actx": "clicca questo testo per prevenire perdita di
          prestazioni quando cambi ad altre finestre/schede", "u_fixed": "OK!  Risolto 👍", "u_cuerr": "caricamento fallito del chunk {0} di {1};\nprobabilmente innocuo, continuo\n\nfile: {2}", "u_cuerr2": "il server ha rifiutato il caricamento (chunk {0} di {1});\nriproverò più tardi\n\nfile: {2}\n\nerrore ", "u_ehstmp": "riproverò; vedi in basso a destra", "u_ehsfin": "il server ha rifiutato la richiesta di finalizzare caricamento; riprovando...", "u_ehssrch": "il server ha rifiutato la richiesta di eseguire ricerca; riprovando...", "u_ehsinit": "il server ha rifiutato la richiesta di iniziare caricamento; riprovando...", "u_eneths": "errore di rete durante handshake per upload; riprovando...", "u_enethd": "errore di rete durante test esistenza target; riprovando...", "u_cbusy": "aspettando che il server si fidi di noi di nuovo dopo un problema di rete...", "u_ehsdf": "il server ha finito lo spazio su disco!\n\ncontinuerò a riprovare, nel caso qualcuno\nliberi abbastanza spazio per continuare", "u_emtleak1": "sembra che il tuo browser possa avere un memory leak;\nper favore", "u_emtleak2": ' passa a https (raccomandato) o ', "u_emtleak3": ' ', "u_emtleakc": 'prova quanto segue:\n
          • premi F5 per aggiornare la pagina
          • poi disabilita il pulsante  mt  nelle  ⚙️ impostazioni
          • e riprova quel caricamento
          I caricamenti saranno un po\' più lenti, ma pazienza.\nScusa per il disturbo !\n\nPS: chrome v107 ha un bugfix per questo', "u_emtleakf": 'prova quanto segue:\n
          • premi F5 per aggiornare la pagina
          • poi abilita 🥔 (patata) nell\'UI caricamento
          • e riprova quel caricamento
          \nPS: firefox avrà sperabilmente un bugfix ad un certo punto', "u_s404": "non trovato sul server", "u_expl": "spiega", "u_maxconn": "la maggior parte dei browser limita questo a 6, ma firefox ti permette di alzarlo con connections-per-server in about:config", "u_tu": '

          AVVISO: turbo abilitato,  client potrebbe non rilevare e riprendere caricamenti incompleti; vedi tooltip pulsante turbo

          ', "u_ts": '

          AVVISO: turbo abilitato,  risultati ricerca possono essere incorretti; vedi tooltip pulsante turbo

          ', "u_turbo_c": "turbo è disabilitato nella configurazione server", "u_turbo_g": "disabilitando turbo perché non hai\nprivilegi di elenco directory all'interno di questo volume", "u_life_cfg": 'auto-elimina dopo min (o ore)', "u_life_est": 'caricamento sarà eliminato ---', "u_life_max": 'questa cartella impone una\nvita massima di {0}', "u_unp_ok": 'unpost è permesso per {0}', "u_unp_ng": 'unpost NON sarà permesso', "ue_ro": 'il tuo accesso a questa cartella è solo-Lettura\n\n', "ue_nl": 'attualmente non sei loggato', "ue_la": 'attualmente sei loggato come "{0}"', "ue_sr": 'attualmente sei in modalità file-search\n\npassa alla modalità upload cliccando la lente d\'ingrandimento 🔎 (accanto al grande pulsante CERCA), e prova a caricare di nuovo\n\nscusa', "ue_ta": 'prova a caricare di nuovo, dovrebbe funzionare ora', "ue_ab": "questo file è già in caricamento in un'altra cartella, e quel caricamento deve essere completato prima che il file possa essere caricato altrove.\n\nPuoi interrompere e dimenticare il caricamento iniziale usando l'🧯 in alto a sinistra", "ur_1uo": "OK: File caricato con successo", "ur_auo": "OK: Tutti i {0} file caricati con successo", "ur_1so": "OK: File trovato sul server", "ur_aso": "OK: Tutti i {0} file trovati sul server", "ur_1un": "Caricamento fallito, scusa", "ur_aun": "Tutti i {0} caricamenti falliti, scusa", "ur_1sn": "File NON trovato sul server", "ur_asn": "I {0} file NON sono stati trovati sul server", "ur_um": "Finito;\n{0} caricamenti OK,\n{1} caricamenti falliti, scusa", "ur_sm": "Finito;\n{0} file trovati sul server,\n{1} file NON trovati sul server", "rc_opn": "apri", //m "rc_ply": "riproduci", //m "rc_pla": "riproduci come audio", //m "rc_txt": "apri nel visualizzatore di file", //m "rc_md": "apri nell’editor di testo", //m "rc_dl": "scarica", //m "rc_zip": "scarica come archivio", //m "rc_cpl": "copia link", //m "rc_del": "elimina", //m "rc_cut": "taglia", //m "rc_cpy": "copia", //m "rc_pst": "incolla", //m "rc_rnm": "rinomina", //m "rc_nfo": "nuova cartella", //m "rc_nfi": "nuovo file", //m "rc_sal": "seleziona tutto", //m "rc_sin": "inverti selezione", //m "rc_shf": "condividi questa cartella", //m "rc_shs": "condividi selezione", //m "lang_set": "aggiornare per rendere effettivo il cambiamento?", "splash": { "a1": "aggiorna", "b1": "ciao   (non sei connesso)", "c1": "disconnetti", "d1": "stato", "d2": "mostra lo stato di tutti i thread attivi", "e1": "ricarica configurazione", "e2": "ricarica i file di configurazione (account/volumi/flag dei volumi),\n e riesegue la scansione di tutti i volumi e2ds.\n\nNota: qualsiasi modifica alle impostazioni globali richiede un riavvio completo per avere effetto", "f1": "puoi visualizzare:", "g1": "puoi caricare su:", "cc1": "altro:", "h1": "disattiva k304", "i1": "attiva k304", "j1": "k304 interrompe la connessione per ogni HTTP 304. Questo aiuta contro alcuni proxy difettosi che possono bloccarsi o smettere improvvisamente di caricare pagine, ma riduce notevolmente le prestazioni", "k1": "resetta impostazioni", "l1": "accedi:", "ls3": "accedi", //m "lu4": "nome utente", //m "lp4": "password", //m "lo3": "disconnetti “{0}” ovunque", //m "lo2": "questo terminerà la sessione su tutti i browser", //m "m1": "bentornato,", "n1": "404: file non trovato  ┐( ´ -`)┌", "o1": "oppure forse non hai accesso? prova una password o torna alla home", "p1": "403: accesso negato  ~┻━┻", "q1": "prova una password o torna alla home", "r1": "torna alla home", ".s1": "mappa", "t1": "azione", "u2": "tempo dall'ultima scrittura sul server\n (caricamento / rinomina / ...)\n\n17d = 17 giorni\n1h23 = 1 ora 23 minuti\n4m56 = 4 minuti 56 secondi", "v1": "connetti", "v2": "usa questo server come un disco locale", "w1": "passa a https", "x1": "cambia password", "y1": "le tue condivisioni", "z1": "sblocca area:", "ta1": "devi prima inserire una nuova password", "ta2": "ripeti per confermare la nuova password:", "ta3": "errore di digitazione; riprova", "nop": "ERRORE: La password non può essere vuota", //m "nou": "ERRORE: Il nome utente e/o la password non possono essere vuoti", //m "aa1": "in arrivo:", "ab1": "disattiva no304", "ac1": "attiva no304", "ad1": "no304 disabilita completamente la cache. Se k304 non è sufficiente, prova questa opzione. Aumenterà notevolmente il consumo di dati!", "ae1": "in uscita:", "af1": "mostra i file caricati di recente", "ag1": "mostra utenti IdP conosciuti", } }; ================================================ FILE: copyparty/web/tl/jpn.js ================================================ //末尾が //m の行は未検証の機械翻訳です Ls.jpn = { "tt": "日本語", "cols": { "c": "アクションボタン", "dur": "間隔", "q": "品質 / ビットレート", "Ac": "オーディオコーデック", "Vc": "ビデオコーデック", "Fmt": "フォーマット / コンテナ", "Ahash": "オーディオチェックサム", "Vhash": "ビデオチェックサム", "Res": "解像度", "T": "ファイル形式", "aq": "オーディオ 品質 / ビットレート", "vq": "ビデオ 品質 / ビットレート", "pixfmt": "サブサンプリング / ピクセル構造", "resw": "水平解像度", "resh": "垂直解像度", "chs": "オーディオチャンネル", "hz": "サンプリングレート", }, "hks": [ [ "misc", ["ESC", "閉じる"], "file-manager", ["G", "リスト / グリッド表示を切り替える"], ["T", "サムネイル / アイコンを切り替える"], ["⇧ A/D", "サムネイルサイズ"], ["ctrl-K", "選択した項目を削除"], ["ctrl-X", "選択範囲をクリップボードに切り取る"], ["ctrl-C", "選択範囲をクリップボードにコピー"], ["ctrl-V", "ここに貼り付け(移動/コピー)"], ["Y", "選択した項目をダウンロード"], ["F2", "選択した項目の名前を変更"], "file-list-sel", ["space", "ファイル選択の切り替え"], ["↑/↓", "選択カーソルを移動"], ["ctrl ↑/↓", "カーソルとビューポートを移動"], ["⇧ ↑/↓", "前/次のファイルを選択"], ["ctrl-A", "すべてのファイル / フォルダを選択"], ], [ "navigation", ["B", "パンくずリスト / ナビペインを切り替える"], ["I/K", "前/次のフォルダ"], ["M", "親フォルダ(または現在のフォルダを展開解除)"], ["V", "ナビペインのフォルダ / テキストファイルを切り替える"], ["A/D", "ナビペインサイズ"], ], [ "audio-player", ["J/L", "前/次の曲"], ["U/O", "10秒前/後スキップ"], ["0..9", "0%~90%へジャンプ"], ["P", "再生/一時停止(開始も)"], ["S", "再生中の曲を選択"], ["Y", "曲をダウンロード"], ], [ "image-viewer", ["J/L, ←/→", "前/次の画像"], ["Home/End", "最初/最後の画像"], ["F", "フルスクリーン"], ["R", "時計回りに回転"], ["⇧ R", "反時計回りに回転"], ["S", "画像を選択"], ["Y", "画像をダウンロード"], ], [ "video-player", ["U/O", "10秒前/後へスキップ"], ["P/K/Space", "再生/一時停止"], ["C", "自動再生"], ["V", "ループ"], ["M", "ミュート"], ["[ and ]", "ループ間隔を設定"], ], [ "textfile-viewer", ["I/K", "前/次のファイル"], ["M", "テキストファイルを閉じる"], ["E", "テキストファイルの編集"], ["S", "ファイルを選択(切り取り/コピー/名前変更)"], ["Y", "テキストファイルをダウンロード"], ["⇧ J", "JSONを整形する"], ] ], "m_ok": "OK", "m_ng": "キャンセル", "enable": "有効", "danger": "危険", "clipped": "クリップボードにコピーされました", "ht_s1": "秒", "ht_s2": "秒", "ht_m1": "分", "ht_m2": "分", "ht_h1": "時間", "ht_h2": "時間", "ht_d1": "日", "ht_d2": "日", "ht_and": " と ", "goh": "コントロールパネル", "gop": '前のフォルダ">prev', "gou": '親フォルダ">up', "gon": '次のフォルダ">next', "logout": "ログアウト ", "login": "ログイン", "access": " アクセス", "ot_close": "サブメニューを閉じる", "ot_search": "`属性、パス/名前、音楽タグ、またはそれらの組み合わせでファイルを検索$N$N`foo bar` = «foo» と «bar» の両方を含める。$N`foo -bar` = «foo» を含み、«bar» を含まない必要があります。$N`^yana .opus$` = «yana» で始まり、«opus» ファイルである$N`"try unite"` = «try unite» だけを含む$N$N日付形式は iso-8601。たとえば、$N`2009-12-31` や `2020-09-12 23:30:00`", "ot_unpost": "unpost: 最近アップロードした投稿を削除するか、未完成の投稿を中止", "ot_bup": "bup: 基本的なアップローダー。Netscape 4.0 もサポートしています。", "ot_mkdir": "mkdir: 新しいディレクトリを作成", "ot_md": "new-file: 新しいテキストファイルを作成", "ot_msg": "msg: サーバーログにメッセージを送信", "ot_mp": "メディアプレーヤー設定", "ot_cfg": "設定オプション", "ot_u2i": 'up2k: ファイルをアップロードする (書き込みアクセス権がある場合)、または検索モードに切り替えてファイルがサーバーのどこかに存在するかどうかを確認$N$Nアップロードは再開可能で、マルチスレッドであり、ファイルのタイムスタンプが保持されますが、[🎈] (基本的なアップローダー) よりも多くの CPU を使用します

          アップロード中、このアイコンは進行状況インジケーターになります。', "ot_u2w": 'up2k: 再開サポート付きのファイルのアップロード(ブラウザを閉じて、後で同じファイルをドロップします)$N$Nマルチスレッドで、ファイルのタイムスタンプが保持されますが、[🎈](基本的なアップローダー)よりも多くの CPU を使用します。

          アップロード中は、このアイコンが進行状況インジケーターになります。', "ot_noie": 'Chrome / Firefox / Edgeを利用してください', "ab_mkdir": "ディレクトリを作成", "ab_mkdoc": "新しいテキストファイル", "ab_msg": "メッセージをサーバーログに送信", "ay_path": "フォルダへジャンプ", "ay_files": "ファイルへジャンプ", "wt_ren": "選択した項目の名前を変更する$Nホットキー: F2", "wt_del": "選択した項目を削除$Nホットキー: ctrl-K", "wt_cut": "選択した項目を切り取る <small>(その後別の場所に貼り付ける)</small>$Nホットキー: ctrl-X", "wt_cpy": "選択した項目をクリップボード$Nにコピー(別の場所に貼り付ける)$Nホットキー: ctrl-C", "wt_pst": "以前に切り取った/コピーした選択範囲を貼り付ける$Nホットキー: ctrl-V", "wt_selall": "すべてのファイルを選択$Nホットキー: ctrl-A(ファイルにフォーカスがあるとき)", "wt_selinv": "選択を反転", "wt_zip1": "このフォルダを圧縮してダウンロード", "wt_selzip": "選択内容を圧縮してダウンロード", "wt_seldl": "選択した項目を個別のファイルとしてダウンロード$Nホットキー: Y", "wt_npirc": "IRC形式のトラック情報をコピーする", "wt_nptxt": "プレーンテキストのトラック情報をコピー", "wt_m3ua": "m3uプレイリストに追加 (後で 📻コピー をクリック)", "wt_m3uc": "m3uプレイリストをクリップボードにコピー", "wt_grid": "グリッド / リスト表示を切り替える$Nホットキー: G", "wt_prev": "前のトラック$Nホットキー: J", "wt_play": "再生 / 一時停止$Nホットキー: P", "wt_next": "次のトラック$Nホットキー: L", "ul_par": "並列アップロード:", "ut_rand": "ファイル名をランダム化する", "ut_u2ts": "最終更新日時のタイムスタンプ$Nファイルシステムからサーバーへコピーする\">📅", "ut_ow": "サーバー上の既存のファイルを上書きする?$N🛡️: しない(代わりに新しいファイル名を生成する)$N🕒: サーバーのファイルが古い場合は上書きする$N♻️: ファイルが異なる場合は常に上書きする$N⏭️: 既存のファイルをすべて無条件にスキップする", "ut_mt": "アップロード中に他のファイルのハッシュを継続する$N$NCPUやHDDがボトルネックになっている場合は無効にしてください", "ut_ask": 'aアップロードを開始する前に確認を求める">💭', "ut_pot": "UIをシンプルにすることで$N低速デバイスでのアップロード速度を向上させる", "ut_srch": "実際にはアップロードせず、代わりにファイルが既にアップロードされているかどうかを確認 $N すでにサーバー上に存在(読み取り可能なすべてのフォルダをスキャン)", "ut_par": "0に設定するとアップロードを一時停止$N$N接続が遅い / 遅延が大きい場合は増やす$N$NLANやサーバーのHDDがボトルネックになっている場合は1にする", "ul_btn": "ファイル / フォルダを
          ここにドロップしてください(またはクリックしてください)", "ul_btnu": "ア ッ プ ロ ー ド", "ul_btns": "検 索", "ul_hash": "ハッシュ", "ul_send": "送信", "ul_done": "完了", "ul_idle1": "キューにはまだアップロードは登録されていない", "ut_etah": "平均 <em>ハッシュ化</em> 速度と完了予想時間", "ut_etau": "平均 <em>アップロード</em> 速度と完了予想時間", "ut_etat": "平均 <em>合計</em> 速度と完了予想時間", "uct_ok": "正常に完了", "uct_ng": "NG: 失敗 / 拒否 / 見つからない", "uct_done": "OKとNGの組み合わせ", "uct_bz": "ハッシュ化またはアップロード", "uct_q": "アイドル状態、保留中", "utl_name": "ファイル名", "utl_ulist": "リスト", "utl_ucopy": "コピー", "utl_links": "リンク", "utl_stat": "状態", "utl_prog": "進捗", // keep short: "utl_404": "404", "utl_err": "エラー", "utl_oserr": "OS-エラー", "utl_found": "発見", "utl_defer": "延期", "utl_yolo": "YOLO", "utl_done": "完了", "ul_flagblk": "ファイルがキューに追加されました
          しかし、別のブラウザタブにはビジー状態のup2kがあり、
          それが終わるまで待機します", "ul_btnlk": "サーバー構成によりこのスイッチはこの状態にロックされています", "udt_up": "アップロード", "udt_srch": "検索", "udt_drop": "ここにドロップ", "u_nav_m": '
          はい、何かもってますか?
          Enter = ファイル(1つ以上)\nESC = 1つのフォルダ(サブフォルダを含む)', "u_nav_b": 'ファイルフォルダ', "cl_opts": "スイッチ", "cl_hfsz": "ファイルサイズ", "cl_themes": "テーマ", "cl_langs": "言語", "cl_ziptype": "フォルダダウロード", "cl_uopts": "up2kスイッチ", "cl_favico": "ファビコン", "cl_bigdir": "ディレクトリの最大数", "cl_hsort": "#ソート", "cl_keytype": "キーの表記タイプ", "cl_hiddenc": "非表示の列", "cl_hidec": "非表示", "cl_reset": "リセット", "cl_hpick": "下の表で非表示にするには列ヘッダーをタップします", "cl_hcancel": "列の非表示を解除", "cl_rcm": "右クリックメニュー", "ct_grid": '田 グリッド', "ct_ttips": '◔ ◡ ◔">ℹ️ ツールチップ', "ct_thumb": 'グリッドビューではアイコンまたはサムネイルを切り替える$Nホットキー: T">🖼️ サムネイル', "ct_csel": 'グリッドビューでファイルを選択するにはCtrlとShiftを使用する。">選択', "ct_dsel": 'グリッドビューでドラッグ選択を使用する。">ドラッグ', //m "ct_dl": 'ファイルをクリックしたときに強制的にダウンロードする(インラインで表示しない)">dl', "ct_ihop": '画像ビューアを閉じたら最後に表示したファイルまでスクロールする。">g⮯', "ct_dots": '隠しファイルを表示する(サーバーが許可している場合)">隠しファイル', "ct_qdel": 'ファイルを削除するときは確認を一度だけ求める">qdel', "ct_dir1st": 'ファイルの前にフォルダを並べ替える">📁 優先', "ct_nsort": '自然ソート(先頭に数字があるファイル名の場合)">nsort', "ct_utc": 'すべての日時をUTCで表示">UTC', "ct_readme": 'フォルダ一覧にREADME.mdを表示">📜 readme', "ct_idxh": 'フォルダ一覧の代わりにindex.htmlを表示">htm', "ct_sbars": 'スクロールバーを表示">⟊', "cut_umod": "サーバー上にファイルが既に存在する場合はサーバーの最終更新タイムスタンプをローカルファイルと一致するように更新します(書き込み+削除権限が必要です)\">re📅", "cut_turbo": "yoloボタンを使用する場合できればこれを有効にしないでください:$N$N大量のファイルをアップロードしていて何らかの理由で再起動する必要があり、できるだけ早くアップロードを続行したい場合にこれを使用します。$N$Nこれはハッシュチェックを単純な"これはサーバー上で同じファイルサイズですか?"に置き換えます。そのため、ファイルの内容が異なる場合はアップロードされません。$N$Nアップロードが完了したらこれをオフにし、同じファイルを再度 "アップロード" してクライアントに検証させる必要があります。\">turbo", "cut_datechk": "turboボタンが有効になっていなければ効果はありません$N$Nyolo要素をわずかに減らしサーバー上のファイルのタイムスタンプが一致するかどうかを確認します。$N$N理論的にはほとんどの未完了 / 破損したアップロードを把握できるはずですが、その後ターボを無効にして検証パスを実行する代わりにはなりません。\">date-chk", "cut_u2sz": "各アップロードチャンクのサイズ(MiB); 大西洋を横断する場合は大きな値の方が飛行効率が良いです。接続の信頼性が低い場合は、小さな値を試してください。", "cut_flag": "一度にアップロードするタブは1つだけにしてください $N -- 他のタブでもこれを有効にする必要があります $N -- 同じドメインのタブにのみ影響します", "cut_az": "ファイルの最小サイズ順ではなくアルファベット順にファイルをアップロードします。$N$アルファベット順にするとサーバー上で何か問題が発生した場合に簡単に確認できるようになりますが、光ファイバー / LANでのアップロードが若干遅くなります。", "cut_nag": "アップロード完了時のOS通知$N(ブラウザまたはタブがアクティブでない場合のみ)", "cut_sfx": "アップロードが完了すると音声アラートが鳴ります$N(ブラウザまたはタブがアクティブでない場合のみ)", "cut_mt": "マルチスレッドを使用してファイルハッシュを高速化する$N$NこれはWebワーカーを使用し$N追加のRAM(最大512 MiB)が必要$N$Nこれによりhttpsは30%高速化、httpは4.5倍高速化r\">mt", "cut_wasm": "ブラウザの組込みのハッシュ関数の代わりにwasmを使用します。Chromeベースのブラウザでは速度が向上しますがCPU負荷が増加します。また、Chromeの古いバージョンの多くにはこれを有効にするとブラウザがすべてのRAMを消費してクラッシュするというバグがあります。\">wasm", "cft_text": "ファビコンテキスト(無効にするには空白にして更新してください)", "cft_fg": "テキストカラー", "cft_bg": "背景色", "cdt_lim": "フォルダに表示するファイルの最大数", "cdt_ask": "一番下までスクロールしたときに$N更にファイルを読み込む代わりに$N何をするか尋ねる", "cdt_hsort": "`メディアURLに含めるソートルール (`,sorthref`) の数。0に設定するとメディアリンクをクリックした際にそのリンクに含まれるソートルールも無視されます。", "cdt_ren": "カスタム右クリックメニューを有効にしてもShiftキーを押しながら右クリックすることで通常のメニューにアクセスできます。\">有効", "cdt_rdb": "カスタム右クリックメニューが開いている状態で再度右クリックしたときに通常のメニューを表示する\">x2", //m "tt_entree": "ナビペインを表示(ディレクトリツリーサイドバー)$Nホットキー: B", "tt_detree": "パンくずリストを表示$Nホットキー: B", "tt_visdir": "選択したフォルダまでスクロール", "tt_ftree": "フォルダツリー / テキストファイルの切り替え$Nホットキー: V", "tt_pdock": "上部のドッキングされたペインに親フォルダを表示", "tt_dynt": "ツリーが拡大するにつれて自動的に増加", "tt_wrap": "単語の折り返し", "tt_hover": "ホバーすると溢れた線を表示する$N( マウスを押さない限りスクロールが中断されます $N  カーソルは左余白です )", "ml_pmode": "フォルダの末尾...", "ml_btns": "コマンド", "ml_tcode": "変換", "ml_tcode2": "この形式に変換", "ml_tint": "色合い", "ml_eq": "オーディオイコライザー", "ml_drc": "ダイナミックレンジコンプレッサー", "ml_ss": "無音をスキップ", //m "mt_loop": "1曲をループ/リピート再生\">🔁", "mt_one": "1曲で止める\">1️⃣", "mt_shuf": "各フォルダ内の曲をシャッフルする\">🔀", "mt_aplay": "サーバーにアクセスするためにクリックしたリンクに曲IDがある場合は自動再生されます$N$Nこれを無効にすると、音楽を再生するときにページのURLが曲IDで更新されなくなります。これにより設定が失われてもURLが残っている場合の自動再生が防止されます。\">a▶", "mt_preload": "ギャップレス再生のために曲の終わり近くに次の曲の読み込みを開始する\">preload", "mt_prescan": "最後の曲が終了する前に次のフォルダへ移動し$Nウェブブラウザが$N再生を停止しないようにする\">nav", "mt_fullpre": "曲全体を事前ロードしてみる;$N✅ 信頼できない接続で有効にする、$N❌ 低速接続では無効にする\">full", "mt_fau": "携帯電話では、次の曲が十分に早く読み込まれない場合に音楽が停止しないようにする(タグの表示が不安定になる可能性があります)\">☕️", "mt_waves": "波形シークバー:$Nスクラバーにオーディオ振幅を表示する\">~s", "mt_npclip": "現在再生中の曲をクリップボードに保存するためのボタンを表示する\">/np", "mt_m3u_c": "選択した曲をm3u8プレイリストエントリとして$Nクリップボードに保存するためのボタンを表示する\">📻", "mt_octl": "OS統合(メディアホットキー / OSD)\">os-ctl", "mt_oseek": "OS統合によるシークを許可する$N$N注: 一部のデバイス(iPhone)では$N次の曲ボタンの代わりになります\">シーク", "mt_oscv": "OSDでアルバムカバーを表示する\">art", "mt_follow": "再生中の曲をスクロールして表示したままにする\">🎯", "mt_compact": "コントローラーを小さく\">⟎", "mt_uncache": "キャッシュクリア  (ブラウザが破損した曲のコピーをキャッシュしているために$N再生できない場合はこれを試してください)\">uncache", "mt_mloop": "開いているフォルダをループ\">🔁 ループ", "mt_mnext": "次のフォルダを読み込んで続行\">📂 次", "mt_mstop": "再生を停止\">⏸ 停止", "mt_cflac": "flac / wavを{0}に変換\">flac", "mt_caac": "aac / m4aを{0}に変換\">aac", "mt_coth": "その他すべて(mp3以外)を{0}に変換\">その他", "mt_c2opus": "デスクトップ、ノート、Androidに最適\">opus", "mt_c2owa": "opus-weba(iOS 17.5以降)\">owa", "mt_c2caf": "opus-caf(iOS 11から17まで)\">caf", "mt_c2mp3": "非常に古いデバイスでこれを使用\">mp3", "mt_c2flac": "最高の音質、ダウンロードサイズが大きい\">flac", "mt_c2wav": "非圧縮再生(さらに大きい)\">wav", "mt_c2ok": "素晴らしい、良い選択です", "mt_c2nd": "これは現在のデバイスに推奨される出力形式ではありませんが、問題ありません", "mt_c2ng": "現在のデバイスはこの出力形式をサポートしていないようですが、とにかく試してみましょう", "mt_xowa": "iOSにはこのフォーマットを使用したバックグラウンド再生を妨げるバグがあります; 代わりにcafまたはmp3を使用してください", "mt_tint": "シークバーの背景レベル(0~100)を調整し$Nバッファリングの邪魔にならにようにする", "mt_eq": "`イコライザーとゲイン制御を有効にします;$N$Nブースト `0` = 標準音量100%(変更なし)$N$N幅 `1  ` = 標準ステレオ(変更なし)$N幅 `0.5` = 左右のクロスフィード50%$N幅 `0  ` = モノラル$N$Nブースト `-0.8` & 幅 `10` = ボーカル除去 :^)$N$Nイコライザーを有効にするとギャップレスアルバムは完全にギャップレスになります。そのため、それを気に場合すべての値をゼロ(幅 = 1を除く)にしてイコライザーをオンにしたままにしてください。", "mt_drc": "ダイナミックレンジコンプレッサー(ボリュームフラットナー / ブリックウォーラー)を有効にします; EQでスパゲッティのバランスをとることもできます。そのため、EQのフィールドを「幅」以外すべて0に設定してください。$N$Nしきい値を超えるオーディオの音量を下げる; しきい値を超えるRATIO dBごとに1dBの出力されますしきい値-24と比率12のデフォルト値は、-22dBを超えることはなく、イコライザーのブーストを0.8に、またはATK 0とRLS 90などの非常に大きな値にした場合1.8まで安全に上げられます (Firefoxでのみ機能し、他のブラウザーではRLSは最大 1 です)。$N$N(Wikipedia を参照してください。もっとわかりやすく説明されています)", "mt_ss": "`無音スキップを有効化;音量が `音` 未満で、再生位置が最初の `始`% または最後の `終`% にある場合、開始/終了付近で再生速度を `速` 倍します", //m "mt_ssvt": "音量しきい値 (0-255)\">音", //m "mt_ssts": "有効しきい値 (トラック%, 開始)\">始", //m "mt_sste": "有効しきい値 (トラック%, 終了)\">終", //m "mt_sssm": "再生速度倍率\">速", //m "mb_play": "再生", "mm_hashplay": "このオーディオファイルを再生しますか?", "mm_m3u": "再生するにはEnter/OKを押します\n編集するにはESC/キャンセルを押してください", "mp_breq": "Firefox 82以降、Chrome 73以降、またはiOS 15以降が必要です", "mm_bload": "読み込み中...", "mm_bconv": "{0}に変換中。お待​​ちください。...", "mm_opusen": "現在のブラウザはaac / m4aファイルを再生できません;\nOpusへのトランスコードが有効になりました", "mm_playerr": "再生に失敗: ", "mm_eabrt": "再生の試行はキャンセルされました", "mm_enet": "インターネット接続が不安定です", "mm_edec": "このファイルは破損している??", "mm_esupp": "現在のブラウザはこのオーディオ形式に対応していません", "mm_eunk": "不明なエラー", "mm_e404": "オーディオを再生できませんでした。エラー404: ファイルが見つかりません。", "mm_e403": "オーディオを再生できませんでした。エラー403: アクセス拒否。\n\nF5キーを押してリロードしてみてください。ログアウトしている可能性があります。", "mm_e415": "オーディオを再生できませんでした。エラー415: ファイルの変換に失敗しました。サーバーログを確認してください。", //m "mm_e500": "オーディオを再生できませんでした。エラー500: サーバーログを確認してください。", "mm_e5xx": "オーディオを再生できませんでした。サーバーエラー ", "mm_nof": "近くにオーディオファイルが見つかりません", "mm_prescan": "次に再生する曲を探しています...", "mm_scank": "次の曲を見つけました:", "mm_uncache": "キャッシュがクリアされました; 次回の再生時にすべての曲が再ダウンロードされます", "mm_hnf": "その曲はもう存在しません", "im_hnf": "その画像はもう存在しません", "f_empty": 'このフォルダは空です', "f_chide": 'これにより列 «{0}» が非表示になります\n\n設定タブで列の非表示を解除できます', "f_bigtxt": "このファイルは {0} MiB の大きさです -- 本当にテキストとして表示しますか?", "f_bigtxt2": "代わりにファイルの末尾だけを表示しませんか?これにより追跡も有効になり、新しく追加されたテキスト行がリアルタイムで表示されます。", "fbd_more": '
          表示中 {0} / {1} ファイル; {2}件を表示 または すべて表示
          ', "fbd_all": '
          表示中 {0} / {1} ファイル; すべて表示
          ', "f_anota": "{1}件のアイテムのうち {0}件が選択されました;\nフォルダ全体を選択するには、まず一番下までスクロールします", "f_dls": '現在のフォルダ内のファイルリンクは\nダウンロードリンクに変更されました', "f_dl_nd": 'フォルダーをスキップしています(代わりに zip/tar ダウンロードを使用してください):\n', //m "f_partial": "現在アップロード中のファイルを安全にダウンロードするには、同じファイル名で.PARTIAL拡張子がないファイルをクリックしてください。これを行うにはキャンセルまたはEscキーを押してください。\n\nOK / Enter を押すとこの警告は無視され、代わりに.PARTIALスクラッチファイルのダウンロードが続行されますが、ほとんどの場合データが破損することになります。", "ft_paste": "{0}件のアイテムを貼り付け$Nホットキー: ctrl-V", "fr_eperm": '名前を変更できません:\nこのフォルダではの"移動"の権限がありません', "fd_eperm": '削除できません:\nこのフォルダではの"削除"の権限がありません', "fc_eperm": '切り取りできません:\nこのフォルダではの"移動"の権限がありません', "fp_eperm": '貼付けできません:\nこのフォルダではの"書込"の権限がありません', "fr_emore": "名前を変更する項目を1つ以上選択してください", "fd_emore": "削除する項目を1つ以上選択してください", "fc_emore": "切り取る項目を1つ以上選択してください", "fcp_emore": "クリップボードにコピーする項目を1つ以上選択してください", "fs_sc": "現在表示中のフォルダを共有する", "fs_ss": "選択したファイルを共有する", "fs_just1d": "複数のフォルダを選択することはできません\nまた、1回の選択でファイルとフォルダを混在させることはできません。", "fs_abrt": "❌ 中止", "fs_rand": "🎲 ランダム名", "fs_go": "✅ 共有を作成", "fs_name": "名前", "fs_src": "ソース", "fs_pwd": "パスワード", "fs_exp": "有効期限", "fs_tmin": "分", "fs_thrs": "時間", "fs_tdays": "日", "fs_never": "無期限", "fs_pname": "任意のリンク名; 空白の場合はランダムになります", "fs_tsrc": "共有するファイルまたはフォルダ", "fs_ppwd": "任意のパスワード", "fs_w8": "共有の作成...", "fs_ok": "Enter/OKを押してクリップボードに保存します\nESC/キャンセルを押して閉じます", "frt_dec": "壊れたファイル名を修正するかもしれません\">url-decode", "frt_rst": "変更したファイル名を元の名前に戻す\">↺ リセット", "frt_abrt": "中止してこのウィンドウを閉じる\">❌ キャンセル", "frb_apply": "名前の変更を適用", "fr_adv": "バッチ / メタデータ / パターン名変更\">詳細設定", "fr_case": "大文字と小文字を区別する正規表現\">case", "fr_win": "Windowsで安全な名前; <>:"\\|?*を日本語の全角文字に置き換える\">win", "fr_slash": "/を新しいフォルダが作成されない文字に置き換える\">no /", "fr_re": "`元のファイル名に適用する正規表現検索パターン; キャプチャグループは、`(1)` や `(2)` のように、以下のフォーマットフィールドで参照することができるので、", "fr_fmt": "`foob​​ar2000 を参考にしています:$N`(title)` は曲名に置き換えられます。$N`[(artist) - ](title)` はアーティストが空白の場合は[この]部分をスキップします。$N`$lpad((tn),2,0)` はトラック番号を2桁にパディングします。", "fr_pdel": "削除", "fr_pnew": "名前をつけて保存", "fr_pname": "新しいプリセットの名前を入力します", "fr_aborted": "中止されました", "fr_lold": "古い名前", "fr_lnew": "新しい名前", "fr_tags": "選択したファイルのタグ(読み取り専用、参照のみ):", "fr_busy": "{0}件のアイテムの名前を変更...\n\n{1}", "fr_efail": "名前の変更に失敗:\n", "fr_nchg": "新しい名前のうち {0}件は、win および/または no / により変更されました。\n\nこれらを変更後の新しい名前で続行しますか?", "fd_ok": "削除成功", "fd_err": "削除失敗:\n", "fd_none": "何も削除されませんでした; サーバー構成 (xbd) によってブロックされた可能性があります。", "fd_busy": "{0}件のアイテムを削除中...\n\n{1}", "fd_warn1": "これら {0}件のアイテムを削除しますか?", "fd_warn2": "最後の警告! 取り消し不可。削除しますか?", "fc_ok": "{0}件のアイテムを切り取り", "fc_warn": '{0}件のアイテムを切り取りました\n\nしかし: このブラウザタブでのみ貼り付けることができます\n(選択範囲が非常に大きいため)', "fcc_ok": "{0}件のアイテムをクリップボードにコピー", "fcc_warn": '{0}件のアイテムをクリップボードにコピーしました\n\nしかし: このブラウザタブでのみ貼り付けることができます\n(選択範囲が非常に大きいため)', "fp_apply": "これらの名前を使用する", "fp_skip": "競合をスキップ", // トピックノート: "既存の名前をスキップ" (対象フォルダ内のファイル名) "fp_ecut": "最初にファイル / フォルダを切り取りまたはコピーし、貼り付け / 移動します\n\n注: 異なるブラウザタブ間で切り取り / 貼り付けが可能です", "fp_ename": "名前がすでに使用されているため、{0}件のアイテムはここに移動することはできません。続行するには、以下に新しい名前を入力するか名前を空白(\"競合をスキップ\")にしてスキップしてください:", "fcp_ename": "名前がすでに使用されているため、{0}件のアイテムはここにコピーすることはできません。続行するには、以下に新しい名前を入力するか名前を空白(\"競合をスキップ\")にしてスキップしてください:", "fp_emore": "まだ修正すべきファイル名の競合がいくつか残っています", "fp_ok": "移動完了", "fcp_ok": "コピー完了", "fp_busy": "{0}件のアイテムを移動中...\n\n{1}", "fcp_busy": "{0}件のアイテムをコピー中...\n\n{1}", "fp_abrt": "中止しています...", "fp_err": "移動に失敗:\n", "fcp_err": "コピーに失敗:\n", "fp_confirm": "これらの{0}件のアイテムをここに移動しますか?", "fcp_confirm": "これらの{0}件のアイテムをここにコピーしますか?", "fp_etab": '他のブラウザタブからクリップボードを読み取ることができませんでした', "fp_name": "デバイスからファイルをアップロードします。ファイル名を入力してください:", "fp_both_m": '
          貼り付けるものを選択
          Enter = «{1}»から{0}件のファイルを移動\nESC = デバイスから{2}件のファイルをアップロード', "fcp_both_m": '
          貼り付けるものを選択
          Enter = «{1}»から{0}件のファイルをコピー\nESC = デバイスから{2}件のファイルをアップロード', "fp_both_b": '移動アップロード', "fcp_both_b": 'コピーアップロード', "mk_noname": "それをする前に左側のテキストフィールドに名前を入力してください :p", "nmd_i1": "必要なファイル拡張子も追加します。例: .md", "nmd_i2": "削除権限がないため、.{0} ファイルのみを作成できます", "tv_load": "テキストドキュメントの読み込み中:\n\n{0}\n\n{1}%({2} / {3} MiB ロード済み)", "tv_xe1": "テキストファイルを読み込めませんでした:\n\nエラー ", "tv_xe2": "404、ファイルが見つかりません", "tv_lst": "テキストファイルの一覧", "tvt_close": "フォルダビューに戻る$Nホットキー: M(または Esc)\">❌ 閉じる", "tvt_dl": "このファイルをダウンロード$Nホットキー: Y\">💾 ダウンロード", "tvt_prev": "前のドキュメントを表示$Nホットキー: i\">⬆ 前へ", "tvt_next": "次のドキュメントを表示$Nホットキー: K\">⬇ 次へ", "tvt_sel": "ファイルの選択  (切り取り/コピー/削除/...)$Nホットキー: S\">選択", "tvt_j": "jsonの整形$Nホットキー: shift-J\">j", "tvt_edit": "テキストエディタでファイルを開く$Nホットキー: E\">✏️ 編集", "tvt_tail": "ファイルの変更を監視する; 新しい行をリアルタイムで表示する\">📡 監視", "tvt_wrap": "ワードラップ\">↵", "tvt_atail": "ページ最下部までスクロールをロック\">⚓", "tvt_ctail": "端末カラーをデコード(ANSIエスケープコード)\">🌈", "tvt_ntail": "スクロールバック制限(読み込むテキストの最大バイト数)", "m3u_add1": "曲がm3uプレイリストに追加されました", "m3u_addn": "{0}曲をm3uプレイリストに追加しました", "m3u_clip": "m3uプレイリストがクリップボードにコピーされました\n\nsomething.m3uという新しいテキストファイルを作成し、そのドキュメントにプレイリストを貼り付けます; これで再生可能になります", "gt_vau": "動画は表示せず音声のみを再生する\">🎧", "gt_msel": "ファイル選択を有効にする; ファイルをCtrlキーを押しながらクリックすると上書き保存します$N$N<em>有効時: ファイル / フォルダをダブルクリックすると開きます</em>$N$Nホットキー: S\">複数選択", "gt_crop": "中央切り抜きのサムネイル\">切抜き", "gt_3x": "高解像度サムネイル\">3x", "gt_zoom": "拡大", "gt_chop": "切断", "gt_sort": "並べ替え", "gt_name": "名前", "gt_sz": "サイズ", "gt_ts": "日付", "gt_ext": "種類", "gt_c1": "ファイル名をさらに短縮する(表示を減らす)", "gt_c2": "ファイル名を短縮する(詳細を表示)", "sm_w8": "検索中...", "sm_prev": "以下の検索結果は以前のクエリからのものです:\n ", "sl_close": "検索結果を閉じる", "sl_hits": "{0}件の結果を表示中", "sl_moar": "さらに読み込む", "s_sz": "サイズ", "s_dt": "日付", "s_rd": "パス", "s_fn": "名前", "s_ta": "タグ", "s_ua": "Up@", "s_ad": "詳細.", "s_s1": "最小 MiB", "s_s2": "最大 MiB", "s_d1": "古い. iso8601", "s_d2": "新しい. iso8601", "s_u1": "アップロード後", "s_u2": "および/または前", "s_r1": "パスに含まれる  (スペース区切り)", "s_f1": "名前に含まれる  (-nopeで否定)", "s_t1": "タグに含まれる  (^=開始、終了=$)", "s_a1": "特定のメタデータプロパティ", "md_eshow": "レンダリングできません ", "md_off": "[📜readme] 無効 [⚙️] -- ドキュメントを非表示", "badreply": "サーバーからの応答を解析できませんでした", "xhr403": "403: アクセスが拒否されました\n\nF5キーを押してみてください。ログアウトしている可能性があります", "xhr0": "不明(サーバーへの接続が失われたか、サーバーがオフラインになっている可能性があります)", "cf_ok": "申し訳ありません -- DD" + wah + "oS 保護が作動し\n\n約30秒で再開されます\n\n何も起こらない場合はF5キーを押してページを再読み込みしてください", "tl_xe1": "サブフォルダを一覧表示できませんでした:\n\nエラー ", "tl_xe2": "404: フォルダーが見つかりません", "fl_xe1": "フォルダ内のファイルを一覧表示できませんでした:\n\nエラー ", "fl_xe2": "404: フォルダーが見つかりません", "fd_xe1": "サブフォルダを作成できませんでした:\n\nエラー ", "fd_xe2": "404: 親フォルダが見つかりません", "fsm_xe1": "メッセージを送信できませんでした:\n\nエラー ", "fsm_xe2": "404: 親フォルダが見つかりません", "fu_xe1": "サーバーから投稿取り消しリストを読み込めませんでした:\n\nエラー ", "fu_xe2": "404: ファイルが見つかりません??", "fz_tar": "非圧縮gnu-tarファイル(Linux / Mac)", "fz_pax": "非圧縮pax形式のtar(遅い)", "fz_targz": "gzip圧縮レベル3のgnu-tar$N$Nこれは通常非常に遅いので$N代わりに非圧縮tarを使用してください。", "fz_tarxz": "xz圧縮レベル1のgnu-tar$N$Nこれは通常非常に遅いので$N代わりに非圧縮のtarを使用してください。", "fz_zip8": "UTF8ファイル名のzip形式(Windows7以前では動作が不安定になる可能性があります)", "fz_zipd": "非常に古いソフトウェア用の従来のcp437ファイル名のzip", "fz_zipc": "cp437はcrc32を早期に計算します$NMS-DOS PKZIP v2.04g用(1993年10月)$N(ダウンロードを開始するまでの処理に時間がかかります)", "un_m1": "最近アップロードした動画を削除したり未完成の動画を中止したりできます。", "un_upd": "更新", "un_m4": "または以下のファイルを共有する:", "un_ulist": "表示", "un_ucopy": "コピー", "un_flt": "任意のフィルター:  URLには以下を含める必要があります", "un_fclr": "フィルターをクリア", "un_derr": '未投稿の削除に失敗しました:\n', "un_f5": '何かが壊れています。リロードするかF5キーを押してみてください。', "un_uf5": "申し訳ありませんが、このアップロードを中止するにはページを更新する必要があります(例:F5キーまたはCtrl+Rキーを押す)", "un_nou": '警告: サーバーが混雑しているため未完了のアップロードを表示できません; しばらくしてから「更新」リンクをクリックしてください', "un_noc": '警告: 完全にアップロードされたファイルの未投稿化はサーバー設定で有効化 / 許可されていません', "un_max": "最初の2000件のファイルを表示中 (フィルターを使用)", "un_avail": "{0}件の最近のアップロードを削除できます
          未完了のものは{1}件中止できます", "un_m2": "アップロード日時順に並べ替え; 新しい順:", "un_no1": "冗談だよ!アップロードされたものはどれも最新じゃない", "un_no2": "冗談だよ!そのフィルターに一致するアップロードに最新のものはありません", "un_next": "次の{0}件のファイルを削除します", "un_abrt": "中止", "un_del": "削除", "un_m3": "最近のアップロードを読み込み中...", "un_busy": "{0}件のファイルを削除中...", "un_clip": "{0}件のリンクをクリップボードにコピーしました", "u_https1": "あなたはするべき", "u_https2": "httpsに切り替える", "u_https3": "パフォーマンス向上のため", "u_ancient": '現在のブラウザは驚くほど古いです -- 代わりにbupを使った方がいいかもしれません', "u_nowork": "Firefox 53以降、Chrome 57以降、またはiOS 11以降が必要です", "tail_2old": "Firefox 105以降、Chrome 71以降、またはiOS 14.5以降が必要です", "u_nodrop": '現在のブラウザは古すぎてドラッグ&ドロップによるアップロードに対応していません', "u_notdir": "それはフォルダではありません!\n\nブラウザが古すぎです、\n代わりにドラッグドロップを試してください", "u_uri": "他のブラウザウィンドウから画像をドラッグアンドドロップするには、\n大きなアップロードボタンにドロップしてください", "u_enpot": 'potato UIに切り替える (アップロード速度が向上する可能性があります)', "u_depot": 'fancy UIに切り替える(アップロード速度が低下する可能性があります)', "u_gotpot": 'アップロード速度を向上させるためにポテトUIに切り替えました。\n\n同意しない場合は遠慮なく元に戻してください!', "u_pott": "

          ファイル:   {0} 完了済み,   {1} 失敗,   {2} ビジー,   {3} キュー内

          ", "u_ever": "これは基本的なアップローダーです; up2kは少なくとも以下が必要です
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'これは基本的なアップローダーです; up2kの方が優れています', "u_uput": '速度を最適化(チェックサムをスキップ)', "u_ewrite": 'このフォルダへの書き込み権限がありません', "u_eread": 'このフォルダーへの読み取りアクセス権がありません', "u_enoi": 'サーバー設定でファイル検索が有効になっていません', "u_enoow": "ここでは上書きは機能しません; 削除権限が必要です", "u_badf": 'これらの {0}件のファイル(合計 {1}件中)はファイルシステムの権限が原因でスキップされた可能性があります:\n\n', "u_blankf": 'これらの {0}件のファイル(合計 {1}件中)は空白です; それでもアップロードしますか?\n\n', "u_applef": 'これらの {0}件のファイル(合計 {1}件中)はおそらく不要です;\n次のファイルをスキップするにはOK/Enterを押してください、\n除外しない場合はキャンセル/ESCを押してそれらもアップロードしてください:\n\n', "u_just1": '\nファイルを1つだけ選択した方がうまくいくかもしれません', "u_ff_many": "Linux / MacOS / Android,を使用している場合この量のファイルによりFirefoxがクラッシュする可能性があります\nその場合はもう一度お試しください(または Chrome を使用してください)。", "u_up_life": "このアップロードは {0}後に\nサーバーから削除されます。", "u_asku": 'これらの{0}件のファイルを{1}にアップロードします', "u_unpt": "左上のボタン🧯を使ってこのアップロードを取り消し / 削除できます", "u_bigtab": '{0}件のファイルを表示しようとしています\n\nブラウザがクラッシュする可能性がありますが、よろしいですか?', "u_scan": 'ファイルをスキャン中...', "u_dirstuck": 'ディレクトリ走査が次の {0}件のアイテムへのアクセス中に停止しました; スキップします:', "u_etadone": '完了 ({0}, {1}件のファイル)', "u_etaprep": '(アップロード準備中)', "u_hashdone": 'ハッシュ化完了', "u_hashing": 'ハッシュ', "u_hs": 'ハンドシェイク中...', "u_started": "ファイルをアップロード中です; 参照 [🚀]", "u_dupdefer": "複製; 他のすべてのファイルの処理完了後に処理されます", "u_actx": "他のウィンドウ/タブに切り替えるときに
          パフォーマンスの低下を防ぐにはこのテキストをクリックしてください", "u_fixed": "OK!  修正しました 👍", "u_cuerr": "{0} / {1} のチャンクのアップロードに失敗しました;\nおそらく無害、継続中\n\nファイル: {2}", "u_cuerr2": "サーバーがアップロードを拒否しました({0} / {1}のチャンク);\n後で再試行します\n\nファイル: {2}\n\nエラー ", "u_ehstmp": "再試行します; 右下を参照", "u_ehsfin": "サーバーがアップロードの完了リクエストを拒否しました; 再試行中...", "u_ehssrch": "サーバーは検索実行リクエストを拒否しました; 再試行中...", "u_ehsinit": "サーバーはアップロード開始リクエストを拒否しました; 再試行中...", "u_eneths": "アップロードハンドシェイク中にネットワークエラーが発生しました; 再試行中...", "u_enethd": "ターゲットの存在をテスト中にネットワークエラーが発生しました; 再試行中...", "u_cbusy": "ネットワークの不具合後サーバーが再び信頼するのを待っています...", "u_ehsdf": "サーバーのディスク容量が不足しました!\n\n誰かが十分な空き領域を確保して続行できるようになるまで\n再試行を続けます", "u_emtleak1": "お使いのウェブブラウザでメモリリークが発生している可能性があります;\nお願いします", "u_emtleak2": ' httpsに切り替える(推奨)もしくは', "u_emtleak3": ' ', "u_emtleakc": '次を試してください:\n
          • F5を押してページを更新してください
          • 次に mtを無効にします   ⚙️ settings
          • ボタンをクリックし
          • もう一度アップロードしてみてください
          アップロードは少し遅くなりますが、仕方ないでしょう。\nご迷惑をおかけして申し訳ありません!\n\n追記: chrome v107ではこの問題が修正されました', "u_emtleakf": '次を試してください:\n
          • F5を押してページを更新してください
          • アップロードUIで🥔(ポテト)を有効にして
          • もう一度アップロードを試してください
          \n追記: firefoxはバグ修正が行われることを期待しています', "u_s404": "サーバー上にファイルが見つかりません", "u_expl": "説明", "u_maxconn": "ほとんどのブラウザではこの数が6に制限されていますが、Firefoxではabout:configconnections-per-serverでこの数を増やすことができます", "u_tu": '

          警告: ターボが有効になっている場合、 クライアントは不完全なアップロードを検出して再開できない可能性があります; ターボボタンのツールチップを見る

          ', "u_ts": '

          警告: ターボが有効になっている場合、 検索結果が間違っている可能性があります; ターボボタンのツールチップを見る

          ', "u_turbo_c": "サーバー設定でターボが無効になっています", "u_turbo_g": "このボリューム内でディレクトリ一覧表示の権限がないため\nターボを無効化しています", "u_life_cfg": '分後に自動削除(もしくは時間)', "u_life_est": 'アップロードは削除されます ---', "u_life_max": 'このフォルダーの最大保存期間が\n{0}に設定されています。', "u_unp_ok": '{0}に対して未投稿が許可されています', "u_unp_ng": '未投稿は許可されません', "ue_ro": 'このフォルダのアクセス権限は読み取り専用です\n\n', "ue_nl": '現在ログインしていません', "ue_la": '現在"{0}"としてログインしています', "ue_sr": '現在ファイル検索モードです\n\n虫眼鏡 🔎(大きな検索ボタンの横)をクリックしてアップロードモードに切り替え、もう一度アップロードしてみてください。\n\n申し訳ありません', "ue_ta": 'もう一度アップロードしてみてください。今度は動作するはずです。', "ue_ab": "このファイルはすでに別のフォルダにアップロード中です。それが完了するまでこのファイルを他の場所にアップロードすることはできません。\n\n左上の🧯を使用して最初のアップロードを中止して忘れることができます。", "ur_1uo": "OK: ファイルのアップロードに成功しました", "ur_auo": "OK: {0}件すべてのファイルが正常にアップロードされました", "ur_1so": "OK: ファイルがサーバー上で見つかりました", "ur_aso": "OK: {0}件すべてのファイルがサーバー上で見つかりました", "ur_1un": "アップロードに失敗しました。申し訳ありません。", "ur_aun": "{0}件すべてのファイルのアップロードに失敗しました。申し訳ありません。", "ur_1sn": "サーバー上にファイルが見つかりません", "ur_asn": "サーバー上に {0}件のファイルが見つかりませんでした", "ur_um": "完了;\n{0}件のアップロードはOKです,\n{1}件のアップロードに失敗しました。申し訳ありません。", "ur_sm": "完了;\n{0}件のファイルがサーバー上に見つかりました,\n{1}件のファイルがサーバー上に見つかりませんでした", "rc_opn": "開く", "rc_ply": "再生", "rc_pla": "オーディオとして再生", "rc_txt": "ファイルビューアで開く", "rc_md": "markdownエディターで開く", "rc_dl": "ダウンロード", "rc_zip": "圧縮してダウンロード", "rc_cpl": "リンクをコピー", "rc_del": "削除", "rc_cut": "切り取り", "rc_cpy": "コピー", "rc_pst": "貼り付け", "rc_rnm": "名前を変更", //m "rc_nfo": "新しいフォルダ", "rc_nfi": "新しいファイル", "rc_sal": "すべて選択", "rc_sin": "選択を反転", "rc_shf": "このフォルダを共有", //m "rc_shs": "選択項目を共有", //m "lang_set": "変更を反映させるために更新しますか?", "splash": { "a1": "更新", "b1": "やあ、見知らぬ人   (あなたはログインしていません)", "c1": "ログアウト", "d1": "dump stack", // トピックノート: "d2" はこのボタンのツールチップです "d2": "すべてのアクティブなスレッドの状態を表示します", "e1": "cfgの再読み込み", "e2": "設定ファイル(アカウント/ボリューム/ボリュームフラグ)を再読み込みして、$Nすべてのe2dsボリュームを再スキャンします$N$N注: グローバル設定の変更を$N有効にするには完全な再起動が必要です", "f1": "閲覧可能:", "g1": "アップロード加納:", "cc1": "その他:", "h1": "k304を無効化", // トピックノート: "j1" はk304が何であるかを説明します "i1": "k304を有効化", "j1": "k304 を有効にすると、HTTP 304ごとにクライアントが切断されバグのあるプロキシがスタックするのを防ぐことができます(突然ページが読み込まれなくなるなど)ただし全体的に速度が低下します", "k1": "クライアント設定をリセット", "l1": "詳しくはログインしてください:", "ls3": "ログイン", "lu4": "ユーザー名", "lp4": "パスワード", "lo3": "「{0}」をどこからでもログアウトする", "lo2": "これによりすべてのブラウザでセッションが終了します", "m1": "おかえりなさい、", // トピックノート: "おかえりなさい、ユーザー名" "n1": "404 not found  ┐( ´ -`)┌", "o1": 'あるいはアクセスできないかもしれません -- パスワードを試すか ホームに戻ってください', "p1": "403 forbiddena  ~┻━┻", "q1": 'パスワードを使うか ホームに戻ってください', "r1": "ホームに戻る", ".s1": "再スキャン", "t1": "アクション", // トピックノート: これは"再スキャン"ボタンの上のヘッダーです "u2": "最後のサーバー書き込みからの時間$N(アップロード / 名前変更 / ...)$N$N17d = 17日$N1h23 = 1時間23分$N4m56 = 4分56秒", "v1": "接続", "v2": "このサーバーをローカルHDDとして使用する", "w1": "httpsへ切り替え", "x1": "パスワードの変更", "y1": "共有の編集", // トピックノート: ユーザーが共有することを決定したフォルダのリストを表示します "z1": "この共有のロックを解除する:", // トピックノート: 隠し共有を表示するためのパスワードプロンプト "ta1": "最初に新しいパスワードを入力してください", "ta2": "確認のため新しいパスワードを再入力してください:", "ta3": "タイプミスを発見しました; もう一度試してください", "nop": "エラー: パスワードは空白にできません", "nou": "エラー: ユーザー名とパスワードは空白にできません", "aa1": "受信ファイル:", "ab1": "no304を無効化", "ac1": "no304を有効化", "ad1": "no304を有効にするとすべてのキャッシュが無効になります; k304 では不十分な場合はこちらをお試しください。これにより膨大な量のネットワークトラフィックが無駄になります!", "ae1": "アクティブなダウンロード:", "af1": "最近のアップロードを表示", "ag1": "idpキャッシュを表示", // トピックノート: IdPユーザーを管理できるページへのリンクです } }; ================================================ FILE: copyparty/web/tl/kor.js ================================================ // //m으로 끝나는 줄은 검증되지 않은 기계 번역입니다 Ls.kor = { "tt": "한국어", "cols": { "c": "작업 버튼", "dur": "길이", "q": "품질/비트레이트", "Ac": "오디오 코덱", "Vc": "비디오 코덱", "Fmt": "형식/컨테이너", "Ahash": "오디오 체크섬", "Vhash": "비디오 체크섬", "Res": "해상도", "T": "파일 유형", "aq": "오디오 품질/비트레이트", "vq": "비디오 품질/비트레이트", "pixfmt": "서브샘플링/픽셀 구조", "resw": "가로 해상도", "resh": "세로 해상도", "chs": "오디오 채널", "hz": "샘플레이트", }, "hks": [ [ "기타", ["ESC", "다양한 창 닫기"], "파일 관리자", ["G", "목록/그리드 보기 전환"], ["T", "썸네일/아이콘 전환"], ["⇧ A/D", "썸네일 이미지 크기"], ["ctrl-K", "선택 항목 삭제"], ["ctrl-X", "선택 항목 잘라내기"], ["ctrl-C", "선택 항목 복사"], ["ctrl-V", "여기에 붙여넣기 (이동/복사)"], ["Y", "선택 항목 다운로드"], ["F2", "선택 항목 이름 바꾸기"], "파일 목록 선택", ["space", "파일 선택/해제"], ["↑/↓", "선택 커서 이동"], ["ctrl ↑/↓", "커서와 뷰포트 동시 이동"], ["⇧ ↑/↓", "이전/다음 파일 선택"], ["ctrl-A", "모든 파일/폴더 선택"], ], [ "탐색", ["B", "브레드크럼/탐색창 전환"], ["I/K", "이전/다음 폴더"], ["M", "상위 폴더 (또는 현재 항목 닫기)"], ["V", "탐색창에 폴더/텍스트 파일 표시 전환"], ["A/D", "탐색창 크기"], ], [ "오디오 플레이어", ["J/L", "이전/다음 곡"], ["U/O", "10초 뒤로/앞으로 건너뛰기"], ["0..9", "0%..90% 지점으로 이동"], ["P", "재생/일시정지 (시작 포함)"], ["S", "재생 중인 곡 선택"], ["Y", "곡 다운로드"], ], [ "이미지 뷰어", ["J/L, ←/→", "이전/다음 이미지"], ["Home/End", "첫/마지막 이미지"], ["F", "전체 화면"], ["R", "시계 방향으로 회전"], ["⇧ R", "반시계 방향으로 회전"], ["S", "이미지 선택"], ["Y", "이미지 다운로드"], ], [ "비디오 플레이어", ["U/O", "10초 뒤로/앞으로 건너뛰기"], ["P/K/Space", "재생/일시정지"], ["C", "다음 파일 계속 재생"], ["V", "반복"], ["M", "음소거"], ["[ 와 ]", "반복 구간 설정"], ], [ "텍스트 파일 뷰어", ["I/K", "이전/다음 파일"], ["M", "텍스트 파일 닫기"], ["E", "텍스트 파일 편집"], ["S", "파일 선택 (잘라내기/복사/이름 바꾸기용)"], ["Y", "텍스트 파일 다운로드"], //m ["⇧ J", "json 미화"], //m ] ], "m_ok": "확인", "m_ng": "취소", "enable": "활성화", "danger": "위험", "clipped": "클립보드에 복사되었습니다", "ht_s1": "초", "ht_s2": "초", "ht_m1": "분", "ht_m2": "분", "ht_h1": "시간", "ht_h2": "시간", "ht_d1": "일", "ht_d2": "일", "ht_and": " ", "goh": "제어판", "gop": '이전 형제 폴더">이전', "gou": '상위 폴더">위로', "gon": '다음 폴더">다음', "logout": "로그아웃 ", "login": "로그인", //m "access": " 액세스", "ot_close": "하위 메뉴 닫기", "ot_search": "`속성, 경로/이름, 음악 태그 또는 이들의 조합으로 파일을 검색합니다.$N$N`foo bar` = «foo»와 «bar»를 모두 포함해야 함,$N`foo -bar` = «foo»는 포함하지만 «bar»는 포함하지 않아야 함,$N`^yana .opus$` = «yana»로 시작하고 «opus» 파일이어야 함$N`"try unite"` = 정확히 «try unite»를 포함해야 함$N$N날짜 형식은 ISO-8601입니다. 예:$N`2009-12-31` 또는 `2020-09-12 23:30:00`", "ot_unpost": "주워담기: 최근 업로드한 항목을 삭제하거나 미완료된 업로드를 중단합니다", "ot_bup": "bup: 기본 업로더. 넷스케이프 4.0도 지원합니다", "ot_mkdir": "mkdir: 새 디렉터리를 만듭니다", "ot_md": "new-file: 새 텍스트 파일을 만듭니다", //m "ot_msg": "msg: 서버 로그에 메시지를 보냅니다", "ot_mp": "미디어 플레이어 옵션", "ot_cfg": "구성 옵션", "ot_u2i": 'up2k: (쓰기 권한이 있는 경우) 파일을 업로드하거나, 검색 모드로 전환하여 서버 어딘가에 파일이 있는지 확인합니다.$N$N업로드는 재개 가능하고, 멀티스레드로 작동하며, 파일 타임스탬프가 보존되지만, [🎈] (기본 업로더)보다 CPU를 더 많이 사용합니다.

          업로드 중에는 이 아이콘이 진행률 표시창이 됩니다!', "ot_u2w": 'up2k: 이어올리기 기능을 지원하는 파일 업로더입니다 (브라우저를 닫았다가 나중에 동일한 파일을 끌어다 놓으세요).$N$N멀티스레드로 작동하며, 파일 타임스탬프가 보존되지만, [🎈] (기본 업로더)보다 CPU를 더 많이 사용합니다.

          업로드 중에는 이 아이콘이 진행률 표시창이 됩니다!', "ot_noie": 'Chrome / Firefox / Edge를 사용해주세요', "ab_mkdir": "디렉터리 만들기", "ab_mkdoc": "새 텍스트 파일", //m "ab_msg": "서버 로그에 메시지 보내기", "ay_path": "폴더로 건너뛰기", "ay_files": "파일로 건너뛰기", "wt_ren": "선택한 항목 이름 바꾸기$N단축키: F2", "wt_del": "선택한 항목 삭제$N단축키: ctrl-K", "wt_cut": "선택한 항목 잘라내기 <small>(다른 곳에 붙여넣기용)</small>$N단축키: ctrl-X", "wt_cpy": "선택한 항목 클립보드에 복사$N(다른 곳에 붙여넣기용)$N단축키: ctrl-C", "wt_pst": "이전에 잘라내거나 복사한 항목 붙여넣기$N단축키: ctrl-V", "wt_selall": "모든 파일 선택$N단축키: ctrl-A (파일에 포커스된 경우)", "wt_selinv": "선택 반전", "wt_zip1": "이 폴더를 압축 파일로 다운로드", "wt_selzip": "선택 항목을 압축 파일로 다운로드", "wt_seldl": "선택 항목을 개별 파일로 다운로드$N단축키: Y", "wt_npirc": "IRC 형식 트랙 정보 복사", "wt_nptxt": "일반 텍스트 트랙 정보 복사", "wt_m3ua": "m3u 재생 목록에 추가 (나중에 📻복사 클릭)", "wt_m3uc": "m3u 재생 목록을 클립보드에 복사", "wt_grid": "그리드/목록 보기 전환$N단축키: G", "wt_prev": "이전 트랙$N단축키: J", "wt_play": "재생/일시정지$N단축키: P", "wt_next": "다음 트랙$N단축키: L", "ul_par": "동시 업로드:", "ut_rand": "파일명 무작위로 만들기", "ut_u2ts": "사용자 파일 시스템의 마지막 수정 타임스탬프를$N서버에 복사\">📅", "ut_ow": "서버에 있는 기존 파일을 덮어쓸까요?$N🛡️: 안 함 (대신 새 파일 이름 생성)$N🕒: 서버 파일이 더 오래된 경우 덮어쓰기$N♻️: 파일이 다르면 항상 덮어쓰기$N⏭️: 기존 파일을 모두 무조건 건너뜀", //m "ut_mt": "업로드 중 다른 파일 해싱 계속하기$N$NCPU 또는 HDD가 병목 현상을 일으키는 경우 비활성화하세요", "ut_ask": '업로드 시작 전 확인 요청">💭', "ut_pot": "느린 기기에서 UI를 단순화하여$N업로드 속도 향상", "ut_srch": "실제로 업로드하는 대신, 파일이 이미 서버에 있는지 확인합니다$N(읽을 수 있는 모든 폴더를 스캔합니다)", "ut_par": "0으로 설정하여 업로드 일시정지$N$N연결이 느리거나 지연 시간이 길면 늘리세요$N$NLAN 환경이거나 서버 HDD가 병목 현상을 일으키면 1로 유지하세요", "ul_btn": "파일/폴더를 여기에
          끌어다 놓거나 클릭하세요", "ul_btnu": "업 로 드", "ul_btns": "검 색", "ul_hash": "해싱", "ul_send": "전송", "ul_done": "완료", "ul_idle1": "대기 중인 업로드가 없습니다", "ut_etah": "평균 <em>해싱</em> 속도 및 예상 완료 시간", "ut_etau": "평균 <em>업로드</em> 속도 및 예상 완료 시간", "ut_etat": "평균 <em>총</em> 속도 및 예상 완료 시간", "uct_ok": "성공적으로 완료됨", "uct_ng": "문제 발생: 실패/거부/찾을 수 없음", "uct_done": "완료됨 (성공 및 문제 발생 포함)", "uct_bz": "해싱 또는 업로드 중", "uct_q": "대기 중, 보류 중", "utl_name": "파일명", "utl_ulist": "목록", "utl_ucopy": "복사", "utl_links": "링크", "utl_stat": "상태", "utl_prog": "진행률", // keep short: "utl_404": "404", "utl_err": "오류", "utl_oserr": "OS 오류", "utl_found": "찾음", "utl_defer": "보류", "utl_yolo": "YOLO", "utl_done": "완료", "ul_flagblk": "파일이 대기열에 추가되었습니다.
          하지만 다른 브라우저 탭에서 up2k가 실행 중이므로,
          해당 작업이 끝날 때까지 기다립니다.", "ul_btnlk": "서버 구성에서 이 스위치를 현재 상태로 잠갔습니다.", "udt_up": "업로드", "udt_srch": "검색", "udt_drop": "여기에 놓으세요", "u_nav_m": '
          자, 갖고 있는 게 무엇인가?
          Enter = 파일 (하나 이상)\nESC = 폴더 하나 (하위 폴더 포함)', "u_nav_b": '파일폴더 하나', "cl_opts": "스위치", "cl_hfsz": "파일 크기", //m "cl_themes": "테마", "cl_langs": "언어", "cl_ziptype": "폴더 다운로드", "cl_uopts": "up2k 스위치", "cl_favico": "파비콘", "cl_bigdir": "큰 디렉터리", "cl_hsort": "#sort", "cl_keytype": "조성 표기법", "cl_hiddenc": "숨겨진 열", "cl_hidec": "숨기기", "cl_reset": "초기화", "cl_hpick": "아래 테이블에서 숨기고 싶은 열의 헤더를 탭하세요", "cl_hcancel": "열 숨기기가 중단되었습니다", "cl_rcm": "우클릭 메뉴", //m "ct_grid": "田 그리드", "ct_ttips": '◔ ◡ ◔">ℹ️ 도움말', "ct_thumb": '그리드 보기에서 아이콘 또는 미리보기 이미지 전환$N단축키: T">🖼️ 미리보기', "ct_csel": '그리드 보기에서 CTRL과 SHIFT를 사용하여 파일 선택">선택', "ct_dsel": '그리드 보기에서 드래그 선택 사용">드래그', //m "ct_dl": '파일을 클릭하면 다운로드를 강제로 수행 (인라인으로 표시하지 않음)">dl', //m "ct_ihop": '이미지 뷰어를 닫으면 마지막으로 본 파일로 스크롤">g⮯', "ct_dots": '숨김 파일 표시 (서버가 허용하는 경우)">숨김파일', "ct_qdel": '파일 삭제 시 한 번만 확인 요청">빠른삭제', "ct_dir1st": '폴더를 파일보다 먼저 정렬">📁 먼저', "ct_nsort": '자연어 정렬 (파일명의 숫자를 인식)">자연어정렬', "ct_utc": '모든 날짜/시간을 UTC로 표시">UTC', "ct_readme": '폴더 목록에 README.md 표시">📜 readme', "ct_idxh": '폴더 목록 대신 index.html 표시">htm', "ct_sbars": '스크롤바 표시">⟊', "cut_umod": '파일이 서버에 이미 있는 경우, 서버의 마지막 수정 타임스탬프를 로컬 파일과 일치하도록 업데이트합니다 (쓰기+삭제 권한 필요).\">re📅', "cut_turbo": 'YOLO 버튼. 아마 활성화하고 싶지 않으실 겁니다.$N$N대량의 파일을 업로드하다가 어떤 이유로 재시작해야 할 때, 최대한 빨리 업로드를 계속하고 싶을 때 사용하세요.$N$N이 옵션은 해시 확인을 단순히 "서버에 동일한 파일 크기를 가진 파일이 있는가?"로 대체하므로, 파일 내용만 다를 경우 업로드되지 않습니다.$N$N업로드가 끝나면 이 옵션을 끄고, 동일한 파일을 다시 \"업로드\"하여 클라이언트가 검증하도록 해야 합니다.\">turbo', "cut_datechk": '터보 버튼이 활성화되어 있지 않으면 효과가 없습니다.$N$NYOLO의 위험성을 약간 줄여줍니다. 서버의 파일 타임스탬프가 사용자의 것과 일치하는지 확인합니다.$N$N이론적으로는 대부분의 미완료/손상된 업로드를 잡아내지만, 터보를 비활성화하고 검증 과정을 거치는 것을 대체할 수는 없습니다.\">날짜확인', "cut_u2sz": "각 업로드 청크의 크기 (MiB)입니다. 큰 값은 태평양을 건너는 데 더 유리합니다. 매우 불안정한 연결에서는 낮은 값을 시도해보세요.", "cut_flag": '한 번에 하나의 탭만 업로드하도록 보장합니다.$N-- 다른 탭도 이 옵션을 활성화해야 합니다.$N-- 동일한 도메인의 탭에만 영향을 미칩니다.', "cut_az": '가장 작은 파일 우선이 아닌 알파벳 순서로 파일을 업로드합니다.$N$N알파벳 순서는 서버에서 문제가 발생했는지 눈으로 확인하기 쉽게 해주지만, 광랜/LAN 환경에서는 업로드 속도가 약간 느려집니다.', "cut_nag": '업로드 완료 시 OS 알림$N(브라우저나 탭이 활성화되지 않은 경우에만)', "cut_sfx": '업로드 완료 시 소리 알림$N(브라우저나 탭이 활성화되지 않은 경우에만)', "cut_mt": '멀티스레딩을 사용하여 파일 해싱 속도를 높입니다.$N$N이 기능은 웹 워커를 사용하며$N더 많은 RAM이 필요합니다 (추가적으로 최대 512 MiB).$N$Nhttps는 30% 더 빠르게, http는 4.5배 더 빠르게 만듭니다.\">mt', "cut_wasm": '브라우저 내장 해셔 대신 wasm을 사용합니다. 크롬 기반 브라우저에서 속도를 향상시키지만 CPU 부하를 증가시키며, 많은 구버전 크롬에는 이 기능을 활성화하면 모든 RAM을 소모하고 충돌하는 버그가 있습니다.\">wasm', "cft_text": "파비콘 텍스트 (비워두고 새로고침하면 비활성화됨)", "cft_fg": "전경색", "cft_bg": "배경색", "cdt_lim": "폴더에 표시할 최대 파일 수", "cdt_ask": "맨 아래로 스크롤할 때$N더 많은 파일을 불러오는 대신$N무엇을 할지 묻기", "cdt_hsort": "`미디어 URL에 포함할 정렬 규칙 (`,sorthref`)의 수. 0으로 설정하면 미디어 링크를 클릭할 때 포함된 정렬 규칙도 무시됩니다.", "cdt_ren": "사용자 지정 우클릭 메뉴를 활성화합니다. shift 키를 누른 채 우클릭하면 기본 메뉴를 사용할 수 있습니다\">활성화", //m "cdt_rdb": "사용자 정의 우클릭 메뉴가 이미 열려 있을 때 다시 우클릭하면 기본 메뉴 표시\">x2", //m "tt_entree": "탐색 창 (디렉터리 트리 사이드바) 표시$N단축키: B", "tt_detree": "이동 경로 표시$N단축키: B", "tt_visdir": "선택한 폴더로 스크롤하기", "tt_ftree": "폴더 트리/텍스트 파일 전환$N단축키: V", "tt_pdock": "상위 폴더를 상단에 고정된 창에 표시", "tt_dynt": "트리가 확장될 때 자동으로 너비 증가", "tt_wrap": "자동 줄 바꿈", "tt_hover": "마우스를 올리면 넘어가는 줄 표시$N(마우스 커서가 왼쪽 여백에$N  있지 않으면 스크롤이 깨짐)", "ml_pmode": "폴더 끝에서...", "ml_btns": "명령", "ml_tcode": "트랜스코딩", "ml_tcode2": "다음으로 트랜스코딩", "ml_tint": "틴트", "ml_eq": "오디오 이퀄라이저", "ml_drc": "다이내믹 레인지 압축기", "ml_ss": "무음 건너뛰기", //m "mt_loop": "한 곡 반복 재생\">🔁", "mt_one": "한 곡 재생 후 중지\">1️⃣", "mt_shuf": "각 폴더의 곡을 무작위 재생\">🔀", "mt_aplay": "서버에 접속한 링크에 곡 ID가 있으면 자동 재생$N$N이것을 비활성화하면 음악 재생 시 페이지 URL이 곡 ID로 업데이트되지 않아, 이 설정이 손실되고 URL이 남아있을 경우 자동 재생되는 것을 방지합니다.\">a▶", "mt_preload": "끊김 없는 재생을 위해 다음 곡을 미리 불러오기 시작\">미리로드", "mt_prescan": "마지막 곡이 끝나기 전에 다음 폴더로 이동하여$N웹브라우저가 재생을 멈추지 않도록 합니다.\">탐색", "mt_fullpre": "전체 곡을 미리 불러오기 시도;$N✅ 불안정한 연결에서 활성화,$N❌ 느린 연결에서는 아마도 비활성화\">전체", "mt_fau": "폰에서 다음 곡이 충분히 빨리 미리 불러오지 않아 음악이 멈추는 것을 방지합니다 (태그 표시가 불안정해질 수 있음).\">☕️", "mt_waves": "파형 탐색 바:$N탐색 바에 오디오 진폭 표시\">~s", "mt_npclip": "현재 재생 중인 곡을 클립보드에 복사하는 버튼 표시\">/np", "mt_m3u_c": "선택한 곡을 m3u8 재생 목록 항목으로$N클립보드에 복사하는 버튼 표시\">📻", "mt_octl": "OS 통합 (미디어 단축키/OSD)\">os-ctl", "mt_oseek": "OS 통합을 통해 탐색 허용$N$N참고: 일부 기기 (iPhone)에서는$N이것이 다음 곡 버튼을 대체합니다.\">탐색", "mt_oscv": "OSD에 앨범 커버 표시\">아트", "mt_follow": "재생 중인 트랙이 보이도록 스크롤 유지\">🎯", "mt_compact": "컴팩트 컨트롤\">⟎", "mt_uncache": "캐시 지우기 (브라우저가 곡의 깨진 사본을 캐시하여$N재생이 안되는 경우 시도해보세요)\">캐시삭제", "mt_mloop": "열린 폴더 반복\">🔁 반복", "mt_mnext": "다음 폴더 불러오고 계속\">📂 다음", "mt_mstop": "재생 중지\">⏸ 중지", "mt_cflac": "flac/wav를 {0}로 변환\">flac", "mt_caac": "aac/m4a를 {0}로 변환\">aac", "mt_coth": "다른 모든 것 (mp3 제외)을 {0}로 변환\">기타", "mt_c2opus": "데스크톱, 노트북, 안드로이드 환경에 최적\">opus", "mt_c2owa": "iOS 17.5 이상용 opus-weba\">owa", "mt_c2caf": "iOS 11부터 17까지용 opus-caf\">caf", "mt_c2mp3": "매우 오래된 기기에서 사용\">mp3", "mt_c2flac": "최고 음질이지만 다운로드 용량이 큼\">flac", "mt_c2wav": "비압축 재생 (더 큼)\">wav", "mt_c2ok": "네, 좋은 선택입니다", "mt_c2nd": "기기에 권장되는 출력 형식이 아니지만 괜찮습니다", "mt_c2ng": "기기가 이 출력 형식을 지원하지 않는 것 같지만, 시도해 보겠습니다", "mt_xowa": "iOS에서 이 형식의 백그라운드 재생이 안되는 버그가 있습니다. 대신 caf나 mp3를 사용해주세요.", "mt_tint": "탐색 바의 배경 레벨 (0-100)$N버퍼링이 덜 눈시리게 만듦", "mt_eq": "`이퀄라이저 및 게인 제어 활성화;$N$Nboost `0` = 표준 100% 볼륨 (수정 없음)$N$Nwidth `1  ` = 표준 스테레오 (수정 없음)$Nwidth `0.5` = 50% 좌우 크로스피드$Nwidth `0  ` = 모노$N$Nboost `-0.8` & width `10` = 보컬 제거 :^)$N$N이퀄라이저를 활성화하면 끊김 없는 앨범이 온전히 끊김 없이 재생되므로, 그 점이 중요하다면 모든 값을 0으로 두고 (width=1 제외) 켜두세요.", "mt_drc": "다이내믹 레인지 컴프레서(볼륨 평탄화/벽돌화)를 활성화합니다. 스파게티의 균형을 맞추기 위해 EQ도 활성화되므로, 원하지 않으면 'width'를 제외한 모든 EQ 필드를 0으로 설정하세요.$N$NTHRESHOLD dB 이상의 오디오 볼륨을 낮춥니다. THRESHOLD를 초과하는 모든 RATIO dB에 대해 1dB의 출력이 있으므로, 기본값인 tresh -24 및 ratio 12는 볼륨이 -22dB보다 커지지 않음을 의미하며, 이퀄라이저 부스트를 0.8 또는 ATK 0과 큰 RLS (예: 90)를 사용하여 1.8까지 안전하게 높일 수 있습니다 (firefox에서만 작동, 다른 브라우저에서는 RLS 최대 1).$N$N(위키백과를 참조하세요, 훨씬 더 잘 설명되어 있습니다)", "mt_ss": "`무음 건너뛰기 활성화; 볼륨이 `음` 미만이고 위치가 처음 `시`% 또는 마지막 `끝`% 이내일 때 시작/끝 근처에서 재생 속도를 `고` 배로 합니다", //m "mt_ssvt": "볼륨 임계값 (0-255)\">음", //m "mt_ssts": "활성 임계값 (% 트랙, 시작)\">시", //m "mt_sste": "활성 임계값 (% 트랙, 끝)\">끝", //m "mt_sssm": "재생 속도 배율\">고", //m "mb_play": "재생", "mm_hashplay": "이 오디오 파일을 재생할까요?", "mm_m3u": "Enter/확인을 눌러 재생\nESC/취소를 눌러 편집", "mp_breq": "Firefox 82+, Chrome 73+ 또는 iOS 15+ 필요", "mm_bload": "불러오는 중...", "mm_bconv": "{0}(으)로 변환 중, 잠시만 기다려주세요...", "mm_opusen": "브라우저가 aac/m4a 파일을 재생할 수 없습니다.\nopus로의 트랜스코딩이 활성화되었습니다.", "mm_playerr": "재생 실패: ", "mm_eabrt": "재생 시도가 취소되었습니다", "mm_enet": "인터넷 연결이 불안정합니다", "mm_edec": "이 파일이 손상된 것 같습니다??", "mm_esupp": "브라우저가 이 오디오 형식을 이해하지 못합니다", "mm_eunk": "알 수 없는 오류", "mm_e404": "오디오를 재생할 수 없습니다; 오류 404: 파일을 찾을 수 없습니다.", "mm_e403": "오디오를 재생할 수 없습니다; 오류 403: 접근이 거부되었습니다.\n\nF5를 눌러 새로고침 해보세요, 로그아웃되었을 수 있습니다", "mm_e415": "오디오를 재생할 수 없습니다; 오류 415: 파일 변환에 실패했습니다; 서버 로그를 확인하세요.", //m "mm_e500": "오디오를 재생할 수 없습니다; 오류 500: 서버 로그를 확인하세요.", "mm_e5xx": "오디오를 재생할 수 없습니다; 서버 오류 ", "mm_nof": "주변에서 더 이상 오디오 파일을 찾을 수 없습니다", "mm_prescan": "다음에 재생할 음악을 찾는 중...", "mm_scank": "다음 곡을 찾았습니다:", "mm_uncache": "캐시가 지워졌습니다. 모든 곡은 다음 재생 시 다시 다운로드됩니다.", "mm_hnf": "그 곡이 더 이상 존재하지 않습니다", "im_hnf": "그 이미지가 더 이상 존재하지 않습니다", "f_empty": '이 폴더는 비어 있습니다', "f_chide": '«{0}» 열을 숨깁니다.\n\n설정 탭에서 열을 다시 표시할 수 있습니다.', "f_bigtxt": "이 파일은 {0} MiB입니다 -- 정말 텍스트로 보시겠습니까?", "f_bigtxt2": "대신 파일의 끝부분만 보시겠습니까? 이렇게 하면 실시간으로 새로 추가되는 텍스트 줄을 보여주는 팔로잉/테일링 기능도 활성화됩니다.", "fbd_more": '
          {1}개 파일 중 {0}개 표시 중; {2}개 더 보기 또는 모두 보기
          ', "fbd_all": '
          {1}개 파일 중 {0}개 표시 중; 모두 보기
          ', "f_anota": "{1}개 항목 중 {0}개만 선택되었습니다.\n전체 폴더를 선택하려면 먼저 맨 아래로 스크롤하세요.", "f_dls": '현재 폴더의 파일 링크가\n다운로드 링크로 변경되었습니다', "f_dl_nd": '폴더를 건너뜁니다 (대신 zip/tar 다운로드를 사용하세요):\n', //m "f_partial": "현재 업로드 중인 파일을 안전하게 다운로드하려면, 파일 이름이 같지만 .PARTIAL 확장자가 없는 파일을 클릭하세요. 이 경고를 무시하려면 \"취소\" 또는 ESC를 누르세요.\n\n\"확인\"/Enter를 누르면 이 경고를 무시하고 .PARTIAL 임시 파일을 계속 다운로드하며, 이 경우 거의 확실히 손상된 데이터를 받게 됩니다.", "ft_paste": "{0}개 항목 붙여넣기$N단축키: ctrl-V", "fr_eperm": "이름을 바꿀 수 없습니다:\n이 폴더에 \"이동\" 권한이 없습니다", "fd_eperm": "삭제할 수 없습니다:\n이 폴더에 \"삭제\" 권한이 없습니다", "fc_eperm": "잘라낼 수 없습니다:\n이 폴더에 \"이동\" 권한이 없습니다", "fp_eperm": "붙여넣을 수 없습니다\n이 폴더에 \"쓰기\" 권한이 없습니다", "fr_emore": "이름을 바꿀 항목을 하나 이상 선택하세요", "fd_emore": "삭제할 항목을 하나 이상 선택하세요", "fc_emore": "잘라낼 항목을 하나 이상 선택하세요", "fcp_emore": "클립보드에 복사할 항목을 하나 이상 선택하세요", "fs_sc": "현재 폴더 공유", "fs_ss": "선택한 파일 공유", "fs_just1d": "하나 이상의 폴더를 선택하거나,\n파일과 폴더를 한 번에 섞어 선택할 수 없습니다", "fs_abrt": "❌ 중단", "fs_rand": "🎲 무작위 이름", "fs_go": "✅ 공유 생성", "fs_name": "이름", "fs_src": "소스", "fs_pwd": "비밀번호", "fs_exp": "만료", "fs_tmin": "분", "fs_thrs": "시간", "fs_tdays": "일", "fs_never": "영원", "fs_pname": "선택적 링크 이름; 비워두면 무작위로 생성", "fs_tsrc": "공유할 파일 또는 폴더", "fs_ppwd": "비밀번호 (선택사항)", "fs_w8": "공유 생성 중...", "fs_ok": "Enter/OK를 눌러 클립보드에 복사\nESC/Cancel를 눌러 닫기", "frt_dec": "깨진 파일 이름의 일부 경우를 수정할 수 있습니다\">url-디코드", "frt_rst": "수정된 파일 이름을 원래대로 되돌립니다\">↺ 초기화", "frt_abrt": "이 창을 중단하고 닫습니다\">❌ 취소", "frb_apply": "이름 바꾸기 적용", "fr_adv": "배치/메타데이터/패턴 이름 바꾸기\">고급", "fr_case": "대소문자 구분 정규식\">대소문자", "fr_win": "Windows 안전 이름; <>:"\\|?*를 일본어 전각 문자로 바꿉니다\">win", "fr_slash": "/를 새 폴더를 만들지 않는 문자로 바꿉니다\">/ 없음", "fr_re": "`원본 파일 이름에 적용할 정규식 검색 패턴; 캡처링 그룹은 아래 형식 필드에서 `(1)`, `(2)` 등으로 참조할 수 있습니다", "fr_fmt": "`foobar2000에서 영감을 받음:$N`(title)`은(는) 곡 제목으로 대체됨,$N`[(artist) - ](title)`은(는) 아티스트가 비어 있으면 [이] 부분을 건너뜀$N`$lpad((tn),2,0)`은(는) 트랙 번호를 2자리로 채움", "fr_pdel": "삭제", "fr_pnew": "다른 이름으로 저장", "fr_pname": "새 프리셋의 이름을 입력하세요", "fr_aborted": "중단됨", "fr_lold": "이전 이름", "fr_lnew": "새 이름", "fr_tags": "선택한 파일의 태그 (읽기 전용, 참조용):", "fr_busy": "{0}개 항목 이름 바꾸는 중...\n\n{1}", "fr_efail": "이름 바꾸기 실패:\n", "fr_nchg": "win 및/또는 / 없음으로 인해 새 이름 중 {0}개가 변경되었습니다.\n\n이 변경된 새 이름으로 계속하시겠습니까?", "fd_ok": "삭제 확인", "fd_err": "삭제 실패:\n", "fd_none": "아무것도 삭제되지 않았습니다. 서버 구성 (xbd)에 의해 차단되었을 수 있습니다.", "fd_busy": "삭제 중 {0}개 항목...\n\n{1}", "fd_warn1": "이 {0}개 항목을 삭제하시겠습니까?", "fd_warn2": "마지막 기회입니다! 되돌릴 수 없습니다. 삭제하시겠습니까?", "fc_ok": "{0}개 항목 잘라내기 완료", "fc_warn": "{0}개 항목 잘라내기 완료\n\n하지만: 선택 항목이 너무 커서 브라우저 탭에서만 붙여넣을 수 있습니다", "fcc_ok": "{0}개 항목을 클립보드에 복사했습니다", "fcc_warn": "{0}개 항목을 클립보드에 복사했습니다\n\n하지만: 선택 항목이 너무 커서 브라우저 탭에서만 붙여넣을 수 있습니다", "fp_apply": "이 이름 사용", "fp_skip": "충돌 건너뛰기", //m "fp_ecut": "붙여넣거나 이동하려면 먼저 파일/폴더를 잘라내거나 복사하세요\n\n참고: 다른 브라우저 탭 간에 잘라내기/붙여넣기를 할 수 있습니다", "fp_ename": "이름이 이미 사용 중이므로 {0}개 항목을 여기로 이동할 수 없습니다. 계속하려면 아래에 새 이름을 지정하거나, 이름을 비워두면 (\"충돌 건너뛰기\") 건너뜁니다:", //m "fcp_ename": "이름이 이미 사용 중이므로 {0}개 항목을 여기로 복사할 수 없습니다. 계속하려면 아래에 새 이름을 지정하거나, 이름을 비워두면 (\"충돌 건너뛰기\") 건너뜁니다:", //m "fp_emore": "아직 해결해야 할 파일 이름 충돌이 남아 있습니다", "fp_ok": "이동 완료", "fcp_ok": "복사 완료", "fp_busy": "{0}개 항목 이동 중...\n\n{1}", "fcp_busy": "{0}개 항목 복사 중...\n\n{1}", "fp_abrt": "취소 중...", "fp_err": "이동 실패:\n", "fcp_err": "복사 실패:\n", "fp_confirm": "이 {0}개 항목을 여기로 이동하시겠습니까?", "fcp_confirm": "이 {0}개 항목을 여기로 복사하시겠습니까?", "fp_etab": '다른 브라우저 탭에서 클립보드를 읽지 못했습니다', "fp_name": "기기에서 파일을 업로드합니다. 이름을 지정하세요:", "fp_both_m": '
          붙여넣을 항목 선택
          Enter = «{1}»에서 파일 {0}개 이동\nESC = 기기에서 파일 {2}개 업로드', "fcp_both_m": '
          붙여넣을 항목 선택
          Enter = «{1}»에서 파일 {0}개 복사\nESC = 기기에서 파일 {2}개 업로드', "fp_both_b": '이동업로드', "fcp_both_b": '복사업로드', "mk_noname": "왼쪽 텍스트 필드에 이름을 먼저 입력해주세요 :p", "nmd_i1": "원하는 파일 확장자를 추가할 수 있습니다. 예: .md", //m "nmd_i2": "삭제 권한이 없어서 .{0} 파일만 만들 수 있습니다", //m "tv_load": "텍스트 문서 불러오는 중:\n\n{0}\n\n{1}% ({3} MiB 중 {2} MiB 로드됨)", "tv_xe1": "텍스트 파일을 불러올 수 없습니다:\n\n오류 ", "tv_xe2": "404, 파일을 찾을 수 없음", "tv_lst": "텍스트 파일 목록", "tvt_close": "폴더 보기로 돌아가기$N단축키: M (또는 Esc)\">❌ 닫기", "tvt_dl": "이 파일 다운로드$N단축키: Y\">💾 다운로드", "tvt_prev": "이전 문서 보기$N단축키: i\">⬆ 이전", "tvt_next": "다음 문서 보기$N단축키: K\">⬇ 다음", "tvt_sel": "파일 선택   (잘라내기/복사/삭제/...용)$N단축키: S\">선택", "tvt_j": "json 미화$N단축키: shift-J\">j", //m "tvt_edit": "텍스트 편집기에서 파일 열기$N단축키: E\">✏️ 편집", "tvt_tail": "파일 변경 사항 모니터링; 실시간으로 새 줄 표시\">📡 팔로우", "tvt_wrap": "자동 줄 바꿈\">↵", "tvt_atail": "페이지 하단으로 스크롤 고정\">⚓", "tvt_ctail": "터미널 색상 디코딩 (ANSI 이스케이프 코드)\">🌈", "tvt_ntail": "스크롤백 제한 (불러온 상태로 유지할 텍스트 바이트 수)", "m3u_add1": "m3u 재생 목록에 곡이 추가되었습니다", "m3u_addn": "{0}개의 곡이 m3u 재생 목록에 추가되었습니다", "m3u_clip": "m3u 재생 목록이 클립보드에 복사되었습니다\n\n something.m3u와 같은 이름의 새 텍스트 파일을 만들고 그 문서에 재생 목록을 붙여넣으면 재생할 수 있습니다.", "gt_vau": "비디오를 표시하지 않고 오디오만 재생\">🎧", "gt_msel": "파일 선택 활성화; ctrl-클릭하여 파일 재정의$N$N<em>활성 시: 파일/폴더를 두 번 클릭하여 열기</em>$N$N단축키: S\">다중선택", "gt_crop": "썸네일 중앙 자르기\">자르기", "gt_3x": "고해상도 썸네일\">3x", "gt_zoom": "확대/축소", "gt_chop": "자르기", "gt_sort": "정렬 기준", "gt_name": "이름", "gt_sz": "크기", "gt_ts": "날짜", "gt_ext": "유형", "gt_c1": "파일명 더 많이 생략하기 (더 적게 표시)", "gt_c2": "파일명 덜 생략하기 (더 많이 표시)", "sm_w8": "검색 중...", "sm_prev": "아래 검색 결과는 이전 검색어에 대한 결과입니다:\n  ", "sl_close": "검색 결과 닫기", "sl_hits": "{0}개 결과 표시 중", "sl_moar": "더 불러오기", "s_sz": "크기", "s_dt": "날짜", "s_rd": "경로", "s_fn": "이름", "s_ta": "태그", "s_ua": "업로드 시점", "s_ad": "고급", "s_s1": "최소 MiB", "s_s2": "최대 MiB", "s_d1": "최소 ISO-8601", "s_d2": "최대 ISO-8601", "s_u1": "이후", "s_u2": "이전", "s_r1": "경로에 포함   (공백으로 구분)", "s_f1": "이름에 포함   (-로 제외)", "s_t1": "태그에 포함   (^=시작, 끝=$)", "s_a1": "특정 메타데이터 속성", "md_eshow": "렌더링할 수 없음 ", "md_off": "[📜readme]가 [⚙️]에서 비활성화됨 -- 문서 숨김", "badreply": "서버로부터의 응답을 구문 분석하지 못했습니다", "xhr403": "403: 접근 거부됨\n\nF5를 눌러보세요, 로그아웃되었을 수 있습니다", "xhr0": "알 수 없음 (서버와의 연결이 끊겼거나 서버가 오프라인일 수 있습니다)", "cf_ok": "죄송합니다 -- DD" + wah + "oS 보호 기능이 작동했습니다\n\n약 30초 후에 다시 정상적으로 작동할 것입니다\n\n아무 일도 일어나지 않으면 F5를 눌러 페이지를 새로고침하세요", "tl_xe1": "하위 폴더를 나열할 수 없습니다:\n\n오류 ", "tl_xe2": "404: 폴더를 찾을 수 없음", "fl_xe1": "폴더의 파일을 나열할 수 없습니다:\n\n오류 ", "fl_xe2": "404: 폴더를 찾을 수 없음", "fd_xe1": "하위 폴더를 만들 수 없습니다:\n\n오류 ", "fd_xe2": "404: 상위 폴더를 찾을 수 없음", "fsm_xe1": "메시지를 보낼 수 없습니다:\n\n오류 ", "fsm_xe2": "404: 상위 폴더를 찾을 수 없음", "fu_xe1": "서버에서 주워담기 목록을 불러오지 못했습니다:\n\n오류 ", "fu_xe2": "404: 파일을 찾을 수 없음??", "fz_tar": "압축되지 않은 gnu-tar 파일 (linux / mac)", "fz_pax": "압축되지 않은 pax 형식 tar (느림)", "fz_targz": "gzip 레벨 3 압축이 적용된 gnu-tar$N$N이것은 보통 매우 느리므로$N압축되지 않은 tar를 대신 사용하세요", "fz_tarxz": "xz 레벨 1 압축이 적용된 gnu-tar$N$N이것은 보통 매우 느리므로$N압축되지 않은 tar를 대신 사용하세요", "fz_zip8": "utf8 파일 이름이 포함된 zip (windows 7 및 이전 버전에서 문제가 있을 수 있음)", "fz_zipd": "정말 오래된 소프트웨어를 위한 전통적인 cp437 파일 이름이 포함된 zip", "fz_zipc": "MS-DOS PKZIP v2.04g (1993년 10월)용으로$Ncrc32가 미리 계산된 cp437$N(다운로드 시작 전 처리 시간이 더 걸림)", "un_m1": "아래에서 최근 업로드를 삭제하거나 미완료된 업로드를 중단할 수 있습니다", "un_upd": "새로고침", "un_m4": "또는 아래에 보이는 파일을 공유할 수 있습니다:", "un_ulist": "보기", "un_ucopy": "복사", "un_flt": "선택적 필터:  URL에 포함되어야 함", "un_fclr": "필터 지우기", "un_derr": '주워담기-삭제 실패:\n', "un_f5": '문제가 발생했습니다, 새로고침하거나 F5를 눌러보세요', "un_uf5": "죄송하지만, 이 업로드를 중단하기 전에 페이지를 새로고침해야 합니다 (예: F5 또는 CTRL-R 누르기).", "un_nou": '경고: 서버가 너무 바빠서 미완료 업로드를 표시할 수 없습니다; 잠시 후 "새로고침" 링크를 클릭하세요', "un_noc": '경고: 완전히 업로드된 파일의 주워담기가 서버 구성에서 활성화/허용되지 않았습니다', "un_max": "처음 2000개 파일을 표시합니다 (필터를 사용하세요)", "un_avail": "{0}개의 최근 업로드를 삭제할 수 있습니다
          {1}개의 미완료 업로드를 중단할 수 있습니다", "un_m2": "업로드 시간순으로 정렬됨. 가장 최근 항목이 먼저 표시:", "un_no1": "아쉽다! 충분히 최근인 업로드가 없습니다.", "un_no2": "아쉽다! 해당 필터와 일치하는 최근 업로드가 없습니다.", "un_next": "아래의 다음 {0}개 파일 삭제", "un_abrt": "중단", "un_del": "삭제", "un_m3": "최근 업로드 로드 중...", "un_busy": "{0}개 파일 삭제 중...", "un_clip": "{0}개의 링크가 클립보드에 복사되었습니다", "u_https1": "더 나은 성능을 위해", "u_https2": "https로 전환", "u_https3": "하는 것이 좋습니다", "u_ancient": '브라우저가 정말 오래되었네요 -- 아마도 bup을 대신 사용해야 할 것 같습니다', "u_nowork": "Firefox 53+, Chrome 57+ 또는 iOS 11+가 필요합니다", "tail_2old": "Firefox 105+, Chrome 71+ 또는 iOS 14.5+가 필요합니다", "u_nodrop": '브라우저가 너무 오래되어 드래그 앤 드롭 업로드를 지원하지 않습니다', "u_notdir": '폴더가 아닙니다!\n\n브라우저가 너무 오래되었습니다,\n대신 드래그드롭을 시도해보세요', "u_uri": '다른 브라우저 창에서 이미지를 드래그드롭하려면,\n큰 업로드 버튼 위로 떨어뜨려주세요', "u_enpot": '단순 UI로 전환 (업로드 속도가 향상될 수 있음)', "u_depot": '화려한 UI로 전환 (업로드 속도가 감소할 수 있음)', "u_gotpot": '업로드 속도 향상을 위해 단순 UI로 전환합니다,\n\n언제든지 다시 전환하셔도 좋습니다!', "u_pott": "

          파일:   {0} 완료,   {1} 실패,   {2} 처리 중,   {3} 대기 중

          ", "u_ever": "이것은 기본 업로더입니다. up2k는 최소한 다음 버전이 필요합니다:
          Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1", "u_su2k": '이것은 기본 업로더입니다. up2k가 더 좋습니다', "u_uput": '속도 최적화 (체크섬 건너뛰기)', "u_ewrite": '이 폴더에 쓰기 권한이 없습니다', "u_eread": '이 폴더에 읽기 권한이 없습니다', "u_enoi": '파일 검색이 서버 구성에서 활성화되지 않았습니다', "u_enoow": '여기서는 덮어쓰기가 작동하지 않습니다. 삭제 권한이 필요합니다', "u_badf": '총 {1}개 중 다음 {0}개의 파일은 파일 시스템 권한 문제 등으로 건너뛰었습니다:\n\n', "u_blankf": '총 {1}개 중 다음 {0}개의 파일은 비어있습니다. 그래도 업로드하시겠습니까?\n\n', "u_applef": '총 {1}개 중 다음 {0}개의 파일은 아마도 불필요한 파일일 것입니다.\n다음 파일을 건너뛰려면 확인/Enter를 누르세요,\n해당 파일도 업로드하려면 취소/ESC를 누르세요:\n\n', "u_just1": '\n파일을 하나만 선택하면 더 잘 작동할 수 있습니다', "u_ff_many": '리눅스/macOS/안드로이드를 사용 중이라면, 이 정도의 파일 수는 Firefox를 충돌시킬 수 있습니다!\n만약 그런 일이 발생하면 다시 시도하거나 Chrome을 사용해주세요.', "u_up_life": '이 업로드는 완료 후 {0} 뒤에\n서버에서 삭제됩니다', "u_asku": '이 {0}개의 파일을 {1}(으)로 업로드하시겠습니까?', "u_unpt": '왼쪽 상단의 🧯를 사용하여 이 업로드를 취소/삭제할 수 있습니다', "u_bigtab": '{0}개의 파일을 표시하려고 합니다\n\n브라우저가 충돌할 수 있습니다, 계속 진행합니까?', "u_scan": '파일 스캔 중...', "u_dirstuck": '디렉터리 반복자가 다음 {0}개 항목에 접근하는 데 실패하여 건너뜁니다:', "u_etadone": '완료 ({0}, {1}개 파일)', "u_etaprep": '(업로드 준비 중)', "u_hashdone": '해싱 완료', "u_hashing": '해시', "u_hs": '핸드셰이킹 중...', "u_started": '파일이 현재 업로드 중입니다. [🚀] 참조', "u_dupdefer": '중복됨. 다른 모든 파일 처리 후 처리됩니다', "u_actx": "다른 창/탭으로 전환 시 성능 저하를
          방지하려면 이 텍스트를 클릭하세요", "u_fixed": "OK!  해결됐습니다 👍", "u_cuerr": "{1} 중 청크 {0} 업로드 실패;\n아마 문제 없을 겁니다. 계속 진행합니다\n\n파일: {2}", "u_cuerr2": "서버가 업로드를 거부했습니다 (청크 {0}/{1});\n나중에 다시 시도합니다\n\n파일: {2}\n\n오류 ", "u_ehstmp": "다시 시도합니다; 오른쪽 하단 참조", "u_ehsfin": "서버가 업로드 완료 요청을 거부했습니다. 재시도 중...", "u_ehssrch": "서버가 검색 수행 요청을 거부했습니다. 재시도 중...", "u_ehsinit": "서버가 업로드 시작 요청을 거부했습니다. 재시도 중...", "u_eneths": "업로드 핸드셰이크 중 네트워크 오류 발생. 재시도 중...", "u_enethd": "대상 존재 여부 테스트 중 네트워크 오류 발생; 재시도 중...", "u_cbusy": '네트워크 문제 후 서버가 다시 우리를 신뢰할 때까지 기다리는 중...', "u_ehsdf": '서버 디스크 공간이 부족합니다!\n\n누군가 계속할 수 있을 만큼의 공간을\n비워줄 경우를 대비해 계속 재시도합니다', "u_emtleak1": "웹 브라우저에 메모리 누수가 있는 것 같습니다.\n", "u_emtleak2": ' https로 전환 (권장)하거나 ', "u_emtleak3": ' ', "u_emtleakc": '다음을 시도해보세요:\n
          • F5를 눌러 페이지를 새로고침하세요
          • 그런 다음 ⚙️ 설정에서  mt  버튼을 비활성화하세요
          • 그리고 다시 그 업로드를 시도해보세요
          업로드가 조금 느려지겠지만, 어쩔 수 없죠.\n불편을 드려 죄송합니다!\n\nPS: 이 버그는 Chrome v107에서 수정되었습니다.', "u_emtleakf": '다음을 시도해보세요:\n
          • F5를 눌러 페이지를 새로고침하세요
          • 그런 다음 업로드 UI에서 🥔(단순 UI)를 활성화하세요
          • 그리고 다시 그 업로드를 시도해보세요
          \nPS: Firefox에서 언젠가 이 버그가 수정될 거라 믿습니다.', "u_s404": '서버에서 찾을 수 없음', "u_expl": '설명', "u_maxconn": "대부분의 브라우저는 이를 6으로 제한하지만, Firefox에서는 about:config에서 connections-per-server 설정값으로 높일 수 있습니다.", "u_tu": '

          경고: 터보가 활성화되어 클라이언트가 불완전한 업로드를 감지하고 재개하지 못할 수 있습니다. 터보 버튼의 툴팁을 참조하세요

          ', "u_ts": '

          경고: 터보가 활성화되어 검색 결과가 부정확할 수 있습니다. 터보 버튼의 툴팁을 참조하세요

          ', "u_turbo_c": "터보가 서버 구성에서 비활성화되었습니다", "u_turbo_g": '이 볼륨 내에서 디렉터리 목록 권한이 없으므로\n터보를 비활성화합니다', "u_life_cfg": '자동 삭제 시간 분 (또는 시간)', "u_life_est": '업로드가 ---에 삭제됩니다', "u_life_max": '이 폴더는 최대 수명을\n{0}(으)로 강제합니다', "u_unp_ok": '주워담기는 {0} 동안 허용됩니다', "u_unp_ng": '주워담기는 허용되지 않습니다', "ue_ro": '이 폴더에 대한 접근은 읽기 전용입니다\n\n', "ue_nl": '현재 로그인되어 있지 않습니다', "ue_la": '현재 \'{0}\'(으)로 로그인되어 있습니다', "ue_sr": '현재 파일 검색 모드입니다\n\n큰 "검색" 버튼 옆의 돋보기 🔎를 클릭하여 업로드 모드로 전환한 후 다시 업로드해보세요\n\n죄송합니다', "ue_ta": '다시 업로드해보세요, 이제 작동할 겁니다', "ue_ab": '이 파일은 이미 다른 폴더로 업로드 중이며, 파일이 다른 곳에 업로드되기 전에 해당 업로드가 완료되어야 합니다.\n\n왼쪽 상단의 🧯를 사용하여 초기 업로드를 중단하고 잊을 수 있습니다.', "ur_1uo": "OK: 파일이 성공적으로 업로드되었습니다", "ur_auo": "OK: 모든 {0}개의 파일이 성공적으로 업로드되었습니다", "ur_1so": "OK: 서버에서 파일을 찾았습니다", "ur_aso": "OK: 서버에서 모든 {0}개의 파일을 찾았습니다", "ur_1un": "업로드에 실패했습니다, 죄송", "ur_aun": "모든 {0}개의 업로드에 실패했습니다, 죄송", "ur_1sn": "서버에서 파일을 찾지 못했습니다", "ur_asn": "서버에서 {0}개의 파일을 찾지 못했습니다", "ur_um": "완료;\n{0}개 업로드 성공,\n{1}개 업로드 실패, 죄송", "ur_sm": "완료;\n서버에서 {0}개 파일 찾음,\n서버에서 {1}개 파일 찾지 못함", "rc_opn": "열기", //m "rc_ply": "재생", //m "rc_pla": "오디오로 재생", //m "rc_txt": "파일 뷰어에서 열기", //m "rc_md": "텍스트 편집기에서 열기", //m "rc_dl": "다운로드", //m "rc_zip": "압축 파일로 다운로드", //m "rc_cpl": "링크 복사", //m "rc_del": "삭제", //m "rc_cut": "잘라내기", //m "rc_cpy": "복사", //m "rc_pst": "붙여넣기", //m "rc_rnm": "이름 변경", //m "rc_nfo": "새 폴더", //m "rc_nfi": "새 파일", //m "rc_sal": "모두 선택", //m "rc_sin": "선택 반전", //m "rc_shf": "이 폴더 공유", //m "rc_shs": "선택 항목 공유", //m "lang_set": '변경 사항을 적용하기 위해 새로고침하시겠습니까?', "splash": { "a1": "새로고침", "b1": "어이 친구! 처음 보는 얼굴인데?   (로그인되어 있지 않습니다)", "c1": "로그아웃", "d1": "스택 덤프하기", "d2": "모든 활성 스레드의 상태를 표시합니다", "e1": "설정 다시 불러오기", "e2": "설정 파일(계정/볼륨/볼륨 플래그)을 다시 불러오고,$N모든 e2ds 볼륨을 다시 스캔합니다$N$N참고: 전역 설정에 대한 변경 사항은$N적용하려면 전체 재시작이 필요합니다", "f1": "탐색 가능한 곳:", "g1": "업로드 가능한 곳:", "cc1": "기타 항목:", "h1": "k304 비활성화", "i1": "k304 활성화", "j1": "k304를 활성화하면 모든 HTTP 304 응답 시 클라이언트 연결이 끊어집니다. 이는 일부 프록시가 멈추는 현상(갑자기 페이지가 로드되지 않음)을 방지할 수 있지만, 대신 전반적인 속도는 느려집니다.", "k1": "클라이언트 설정 초기화", "l1": "로그인하기:", "ls3": "로그인", //m "lu4": "사용자 이름", //m "lp4": "비밀번호", //m "lo3": "{0}을(를) 모든 곳에서 로그아웃", //m "lo2": "이 작업은 모든 브라우저에서 세션을 종료합니다", //m "m1": "또 오셨네요,", "n1": "404 찾을 수 없음  ┐( ´ -`)┌", "o1": "또는 접근 권한이 없을 수 있습니다. 비밀번호를 입력하거나 홈으로 이동하세요", "p1": "403 접근 금지  ~┻━┻", "q1": "비밀번호를 입력하거나 홈으로 이동하세요", "r1": "홈으로 이동", ".s1": "다시 스캔", "t1": "작업", "u2": "서버에 마지막으로 쓰기 작업을 한 후 경과된 시간$N(업로드 / 이름 변경 / 등등...)$N$N17d = 17일$N1h23 = 1시간 23분$N4m56 = 4분 56초", "v1": "연결", "v2": "이 서버를 로컬 하드디스크처럼 사용하기", "w1": "HTTPS로 전환", "x1": "비밀번호 변경", "y1": "공유 설정", "z1": "이 공유 잠금해제:", "ta1": "새 비밀번호를 먼저 입력하세요", "ta2": "새 비밀번호 확인을 위해 다시 입력하세요:", "ta3": "오타가 있습니다. 다시 시도해주세요", "nop": "오류: 비밀번호를 비워 둘 수 없습니다", //m "nou": "오류: 사용자 이름 및/또는 비밀번호를 비워 둘 수 없습니다", //m "aa1": "수신 중인 파일:", "ab1": "no304 비활성화", "ac1": "no304 활성화", "ad1": "no304를 활성화하면 모든 캐싱이 비활성화됩니다. k304로 충분하지 않은 경우 시도해보세요. 네트워크 트래픽이 대량으로 낭비됩니다!", "ae1": "활성 다운로드:", "af1": "최근 업로드 보기", "ag1": "IdP 캐시 보기", } }; ================================================ FILE: copyparty/web/tl/nld.js ================================================ // Regels die eindigen op //m zijn niet-geverifieerde machinale vertalingen Ls.nld = { "tt": "Nederlands", "cols": { "c": "Action knoppen", "dur": "Duratie", "q": "Kwaliteit / bitrate", "Ac": "Audio codec", "Vc": "Video codec", "Fmt": "Formaat / container", "Ahash": "Audio checksum", "Vhash": "Video checksum", "Res": "Resolution", "T": "Bestandstype", "aq": "Audio kwaliteit / bitrate", "vq": "Video kwaliteit / bitrate", "pixfmt": "Subsampling / pixel structure", "resw": "Horizontale resolutie", "resh": "Verticale resolutie", "chs": "Audiokanalen", "hz": "Samplefrequentie", }, "hks": [ [ "diversen", ["ESC", "Sluit verschillende dingen"], "bestand beheer", ["G", "Verwissel tussen list / grid weergave"], ["T", "Verwissel tussen miniaturen / iconen"], ["⇧ A/D", "Thumbnail formaat"], ["ctrl-K", "Verwijder geselecteerde"], ["ctrl-X", "Knip selectie naar klembord"], ["ctrl-C", "Kopieer selectie naar klembord"], ["ctrl-V", "Hier plakken (verplaatsen/kopieëren)"], ["Y", "Download geselecteerde"], ["F2", "Hernoem geselecteerde"], "bestand-lijst-selectie", ["space", "wissel bestand selectie"], ["↑/↓", "verplaats selectie cursor"], ["ctrl ↑/↓", "verplaats cursor en scherm"], ["⇧ ↑/↓", "select vorige/volgende bestand"], ["ctrl-A", "selecteer alle bestanden / mappen"], ], [ "navigatie", ["B", "verwissel breadcrumbs / navpane"], ["I/K", "Vorige/volgende map"], ["M", "Bovenliggende map (of huidige uitvouwen)"], ["V", "Berwissel map / tekstbestand in navpane"], ["A/D", "Navpane formaat"], ], [ "muziek-speler", ["J/L", "Vorige/volgende song"], ["U/O", "Skip 10sec terug/vooruit"], ["0..9", "Spring naar 0%..90%"], ["P", "Speel/pauzeer (start ook)"], ["S", "Selecteer afspelende song"], ["Y", "Download song"], ], [ "afbeelding viewer", ["J/L, ←/→", "Vorige/volgende afbeelding"], ["Home/End", "Eerste/laatste afbeelding"], ["F", "Volledig scherm"], ["R", "Draai rechtsom"], ["⇧ R", "Draai linksom"], ["S", "Selecteer afbeelding"], ["Y", "Download afbeelding"], ], [ "video-speler", ["U/O", "Skip 10sec terug/vooruit"], ["P/K/Space", "Speel/pauze"], ["C", "Verder met volgende"], ["V", "herhaal"], ["M", "stil"], ["[ and ]", "zet herhaal interval"], ], [ "tekstbestand-viewer", ["I/K", "vorige/volgende bestand"], ["M", "sluit tekst bestand"], ["E", "bewerk tekst bestand"], ["S", "selecteer bestand (voor knip/kopie/hernoem)"], ["Y", "tekst bestand downloaden"], //m ["⇧ J", "json verfraaien"], //m ] ], "m_ok": "OK", "m_ng": "Annuleren", "enable": "Inschakelen", "danger": "GEVAARLIJK", "clipped": "Gekopieërd naar klembord", "ht_s1": "seconde", "ht_s2": "secondes", "ht_m1": "minuut", "ht_m2": "minuten", "ht_h1": "uur", "ht_h2": "uur", "ht_d1": "dag", "ht_d2": "dagen", "ht_and": " en ", "goh": "Beheer-paneel", "gop": 'Vorige map">Vorige', "gou": 'Bovenligende map">Omhoog', "gon": 'Volgende map">Volgende', "logout": "Uitloggen ", "login": "Inloggen", //m "access": " Toegang", "ot_close": "Sluit onder-menu", "ot_search": "`Zoek voor bestanden bij attributes, pad / naam, muziek tags, of elk andere combinatie tussen$N$N`foo bar` = moet beide «foo» en «bar» bevatten,$N`foo -bar` = moet «foo» bevatten maar geen «bar»,$N`^yana .opus$` = start met «yana» en moet een «opus» bestand zijn$N`"try unite"` = moet precies «try unite» bevatten$N$Nde datum formaat is iso-8601, zoals$N`2009-12-31` of `2020-09-12 23:30:00`", "ot_unpost": "unpost: verwijder je recente uploads, of onvoltooide uploads afbreken", "ot_bup": "bup: Basisuploader, supports zelfs netscape 4.0", "ot_mkdir": "mkdir: Maak een nieuwe map", "ot_md": "new-file: Maak een nieuw tekstbestand", //m "ot_msg": "msg: Verstuur een bericht naar de server logs", "ot_mp": "Media speler opties", "ot_cfg": "Configuratie opties", "ot_u2i": 'up2k: upload bestanden (als je schrijf toegang hebt) of verwissel naar zoek-mode om te zien of ze ergens bestaan op de server$N$Nuploads zijn hervatbaar, multithreaded, en bestandstijdstempels blijven behouden, maar het gebruikt meer CPU dan [🎈]  (de basic uploader)

          tijdens het uploaden, dit icoon word dan een progress indicatie!', "ot_u2w": 'up2k: upload bestanden met hervattings ondersteuning (sluit je webbrowser en selecteer dezelfde bestand later opnieuw)$N$Nmultithreaded, en bestandstijdstempels blijven behouden, maar het gebruikt meer CPU dan [🎈]  (de basic uploader)

          tijdens het uploaden, dit icoon word dan een progress indicatie!', "ot_noie": 'Gebruik alstublieft Chrome / Firefox / Edge', "ab_mkdir": "maak map", "ab_mkdoc": "nieuw tekstbestand", //m "ab_msg": "verstuur msg naar srv log", "ay_path": "skip naar mappen", "ay_files": "skip naar bestanden", "wt_ren": "Hernoem geselecteerde items$NHotkey: F2", "wt_del": "Berwijder geselecteerde items$NHotkey: ctrl-K", "wt_cut": "Knip geselecteerde items <small>(en plak het ergens anders)</small>$NHotkey: ctrl-X", "wt_cpy": "Kopieer geselecteerde items naar klembord$N(om te plakken ergens anders)$NHotkey: ctrl-C", "wt_pst": "Plak eeen laatst geknipte / gekopieërde selectie$NHotkey: ctrl-V", "wt_selall": "Selecteer alle bestanden$NHotkey: ctrl-A (wanneer bestand gefocused is)", "wt_selinv": "Selectie omkeren", "wt_zip1": "Download deze map als archief", "wt_selzip": "Download selectie als archief", "wt_seldl": "Download selectie als losse bestanden$NHotkey: Y", "wt_npirc": "Kopieer irc-geformarteerde track info", "wt_nptxt": "Kopieer platte tekst track info", "wt_m3ua": "Aan m3u afspeellijst toevoegen (klik 📻kopieer later)", "wt_m3uc": "Kopieer m3u playlist naar klembord", "wt_grid": "Verwissel grid / lijst weergave$NHotkey: G", "wt_prev": "Vorig nummer$NHotkey: J", "wt_play": "Afspelen / pauzeer$NHotkey: P", "wt_next": "Volgend nummer$NHotkey: L", "ul_par": "Parallel uploads:", "ut_rand": "Willekeurige bestandsnaam", "ut_u2ts": "Kopieer de laatste-gewijzigde tijdstamp$Nvan je bestandsysteem naar de server\">📅", "ut_ow": "Overschrijf bestaande bestanden op de server?$N🛡️: nooit (zal in plaats daarvan een nieuwe bestandsnaam genereren)$N🕒: overschrijven als de server-bestand ouder is dan het geüploade bestand$N♻️: altijd overschrijven als de bestanden verschillend zijn$N⏭️: alle bestaande bestanden onvoorwaardelijk overslaan", //m "ut_mt": "Ga door met hashen van andere bestanden tijdens het uploaden$N$Moet je misschien uitschakelen als je CPU of HDD het niet aan kan", "ut_ask": 'Vraag voor bevestiging voordat het uploaden start">💭', "ut_pot": "Verbeter de uploadsnelheid voor langzame apparaten$Ndoor de interface minder complex te maken", "ut_srch": "Niet uploaden, maar check of de bestanden als op de server bestaan$N (checkt alle mappen die waar jij toegang op hebt)", "ut_par": "Pauzeer bij zetten het op 0$N$Nverhoog als je verbinding traag is$N$Nhou het op 1 als je netwerk of server HDD het niet aankan", "ul_btn": "Drop bestanden / mappen
          hier (of klik mij)", "ul_btnu": "U P L O A D", "ul_btns": "Z O E K E N", "ul_hash": "Hashing", "ul_send": "Versturen", "ul_done": "Klaar", "ul_idle1": "Geen uploads in wachtrij", "ut_etah": "Gemiddelde <em>hashing</em> snelheid en geschatte tijd tot de voltooiing", "ut_etau": "Gemiddelde <em>verzend</em> snelheid en geschatte tijd tot voltooiing", "ut_etat": "Gemiddelde <em>totale</em> snelheid en geschatte tijd tot voltooiing", "uct_ok": "Succesvol afgerond", "uct_ng": "Niet goed: gefaald / geweigerd / niet gevonden", "uct_done": "ok en ng gecombineerd", "uct_bz": "Hashing van uploads", "uct_q": "Inactief, in afwachting", "utl_name": "Bestandsnaam", "utl_ulist": "Lijst", "utl_ucopy": "Kopieer", "utl_links": "Links", "utl_stat": "Status", "utl_prog": "Vooruitgang", // keep short: "utl_404": "404", "utl_err": "FOUT", "utl_oserr": "OS-FOUT", "utl_found": "gevonden", "utl_defer": "Uitgesteld", "utl_yolo": "YOLO", "utl_done": "klaar", "ul_flagblk": "De bestanden zijn toegevoegd aan de wachtrij
          maar er is een drukke up2k bezig in een andere tabblad,
          wachten totdat die eerst klaar is", "ul_btnlk": "De server configuratie heeft deze schakelaar versleuteld in deze staat", "udt_up": "Upload", "udt_srch": "Zoeken", "udt_drop": "Laat hier los", "u_nav_m": '
          Hey, wat heb jij daar?
          Enter = Bestanden (een of meer)\nESC = Een map (inclusief submappen)', "u_nav_b": 'BestandenEen map', "cl_opts": "Switches", "cl_hfsz": "Bestandsgrootte", //m "cl_themes": "Thema", "cl_langs": "Taal", "cl_ziptype": "Download map als", "cl_uopts": "up2k switches", "cl_favico": "Favicon", "cl_bigdir": "Item limiet in map", "cl_hsort": "#sorteer", "cl_keytype": "Key notaties", "cl_hiddenc": "Verborgen kolomen", "cl_hidec": "Verborgen", "cl_reset": "Reset", "cl_hpick": "Tik op de kolomkoppen om ze in de onderstaande tabel te verbergen", "cl_hcancel": "Kolumn verbergen geannuleerd", "cl_rcm": "Rechtermuisknopmenu", //m "ct_grid": '田 grid', "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'In grid-overzicht, wissel tussen iconen of thumbnails$NHotkey: T">🖼️ thumbs', "ct_csel": 'Gebruik CTRL en SHIFT voor de bestand selectie in grid-overzicht>sel', "ct_dsel": 'Gebruik slepen om te selecteren in grid-overzicht>slepen', //m "ct_dl": 'download afdwingen (niet inline weergeven) wanneer op een bestand wordt geklikt">dl', //m "ct_ihop": 'Als je afbeeldingviewer afsluit, scroll omlaag naar de laatst bekeken bestand">g⮯', "ct_dots": 'Laat verborgen bestanden zien (als de server dat toestaat)">dotfiles', "ct_qdel": 'Waneeer je een bestand verwijderd, vraag eenmalig om bevestiging">qdel', "ct_dir1st": 'Sorteer mappen eerst en dan de bestanden">📁 first', "ct_nsort": 'Natural sort (voor bestandsnamen dat beginnen met getallen)">nsort', "ct_readme": 'Laat README.md in mappen lijst zien">📜 readme', "ct_utc": 'Toon alle datums en tijden in UTC">UTC', "ct_idxh": 'Laat index.html zien in plaats van de map overzicht">htm', "ct_sbars": 'Laat scrollbars zien">⟊', "cut_umod": "Als een bestand al bestaat op de server, update de 'gewijzigd' waarde op het bestand wat op de server staat met het bestand wat je geupload hebt (vereist schrijf+verwijder rechten)\">re📅", "cut_turbo": "De yolo knop, die wil jij waarschijnlijk NIET actief wilt hebben:$N$Ngebruik dit als je heel veel bestanden gaat uploaden EN je moest het herstarten voor een reden en je wilt doorgaan met uploaden ASAP$N$Ndit vervangt de hash-check met een simpele "heeft dit dezelfde bestands groote op de server?", zo als de bestands inhoud verschillend is, dan worden ze NIET geupload$N$NJe zou deze optie weer uit moeten zetten als de upload klaar is en dan "upload" de zelfde bestanden opnieuw uploaden zo de client het kan verifieren\">turbo", "cut_datechk": "Heeft geen effect tenzij de turbo knop actief is$N$Nverminder de yolo factor (een klein beetje); controlleert of de bestand tijdstamp op de server hetzelfde is met het geuploade bestand$N$Ndit zou in theorie de meest onvoltooide/onvoledige uploads, maar dit is geen vervaning voor de verificatie-check met de turbo knop uitgeschakeld daarna\">date-chk", "cut_u2sz": "Grote (in MiB) voor elk geuploade stuk; grote waardes vliegen beter over de Atlantische Oceaan. Probeer lage waardes op zeer onstabiele verbindingen", "cut_flag": "Alleen een tabblad kan bestanden uploaden $N -- andere tabbladen moeten deze optie ook actief hebben $N -- dit heeft alleen effect op de tabbladen die op hetzelfde domain zijn", "cut_az": "Bestanden uploaden in alfabetische volgorde, in plaats van kleinste bestanden eerst$N$Nalfabetische volgorde kan het makkelijker maken om te zien of er wat fout is gegaan op de server, dit maakt het uploaden ietsjes trager op fiber / LAN", "cut_nag": "Systeem notificatie weergeven als een upload voltooid is$N(alleen als de browser of tabblad niet actief is)", "cut_sfx": "Geluid waarschuwing afspelen als een upload voltooid is$N(alleen als de browser of tabblad niet actief is)", "cut_mt": "Gebruik multithreading om bestands-hashing te versnellen$N$Ndit gebruikt web-workers en vereist$Nmeer geheugen (tot wel 512 MiB extra)$N$Nmaakt https 30% sneller en http 4.5x sneller\">mt", "cut_wasm": "Gebruik wasm in plaats van de webbrowser ingebouwde hasher; verbetert de snelheid op chrome-gebaseerde webbrowsers maar verhoogd CPU gebruik, veel oude versie van chrome hebben een bug dat een geheugen lek heeft, dat kan alle geheugen in gebruik nemen en crashen als dit actief is\">wasm", "cft_text": "Favicon tekst (laat leeg en vernieuw om uit te schakelen)", "cft_fg": "Voorgrondkleur", "cft_bg": "Achtergrondkleur", "cdt_lim": "Max aantal bestanden laten zien in een map", "cdt_ask": "Als helemaal naar beneden gescrolld bent,$Nin plaats van meer inladen,$Nvraag wat het moet doen", "cdt_hsort": "`Hoeveel sorteerregels (`,sorthref`) moeten er in media-URL's worden opgenomen? Als je dit op 0 instelt, worden de sorteerregels in medialinks ook genegeerd wanneer erop geklikt word.", "cdt_ren": "Aangepast rechtermuisknopmenu inschakelen, het normale menu blijft beschikbaar met shift + rechtermuisknop\">inschakelen", //m "cdt_rdb": "toon het normale rechtermuisknopmenu wanneer het aangepaste al open is en opnieuw wordt geklikt\">x2", //m "tt_entree": "Laat navpane zien (directoryboom zijbalk)$NHotkey: B", "tt_detree": "Laat breadcrumbs zien$NHotkey: B", "tt_visdir": "Scroll naar geselecteerde map", "tt_ftree": "Verwissel tussen directoryboom / tekst bestanden$NHotkey: V", "tt_pdock": "Laat bovenliggende mappen zien in een vastgezet deelvenster bovenaan", "tt_dynt": "Automatisch groeien naarmate de directoryboom zich uitbreidt", "tt_wrap": "Automatische terugloop", "tt_hover": "Laat overlopenden lijnen zien bij zweven$N(stopt het scrollen tenzij de muis in de linker gedeelte van het scherm is)", "ml_pmode": "Aan het einde van de map...", "ml_btns": "Cmds", "ml_tcode": "Transcode", "ml_tcode2": "Transcode naar", "ml_tint": "Tint", "ml_eq": "Audio-equalizer", "ml_drc": "Dynamisch bereikcompressor", "ml_ss": "Stiltes overslaan", //m "mt_loop": "Loop/herhaal een nummer\">🔁", "mt_one": "Stop na een nummer\">1️⃣", "mt_shuf": "Shuffle alle muziek in alle mappen\">🔀", "mt_aplay": "Autoplay als er een song-ID staat in de link waarop je hebt geklikt om naar de server te gaan$N$NAls u dit uitschakelt, wordt de pagina-URL ook niet meer bijgewerkt met nummer-ID's tijdens het afspelen van muziek. Dit voorkomt automatisch afspelen als deze instellingen verloren gaan, maar de URL behouden blijft.\">a▶", "mt_preload": "Begin het laden van de volgende nummer vlak voordat de huidige nummer het einde bereikt voor gapless playback\">preload", "mt_prescan": "Ga naar de volgende map voordat de laatste nummer eindigd$NMaakt de webbrower blij$NZo het afspelen van muziek niet gestopt word\">nav", "mt_fullpre": "Probeer het hele nummer vooraf te laden;$N✅ activeer dit op onstabiele verbindingen,$N❌ zet uit als je waarschijnlijk een trage verbinding hebt\">full", "mt_fau": "Op telefoons, voorkom muziek van stoppen als de volgende nummer niet snel genoeg voorgeladen is (kan de weergave van tags glitchy maken)\">☕️", "mt_waves": "Waveform zoekbar:$NToon audio-amplitude in de zoekbar\">~s", "mt_npclip": "Knoppen tonen voor het clipboarden van het nummer dat op dat moment wordt afgespeeld\">/np", "mt_m3u_c": "Knoppen tonen om de geselecteerde nummers als m3u8-afspeellijstitems te clipboarden\">📻", "mt_octl": "OS-integratie (media hotkeys / osd)\">os-ctl", "mt_oseek": "Zoeken via os-integratie mogelijk maken$N$NNotitie: op sommige toestellen (iPhones) dit vervcangt de volgende-nummer knop\">seek", "mt_oscv": "Albumhoes weergeven in osd\">art", "mt_follow": "Het afgespeelde nummer in beeld houden\">🎯", "mt_compact": "Compacte bedieningselementen\">⟎", "mt_uncache": "Cache wissen  (Probeer dit als uw browser een kapotte kopie van een nummer heeft gecached, waardoor het niet afgespeeld kan worden)\">uncache", "mt_mloop": "De open map herhalen\">🔁 loop", "mt_mnext": "Laad de volgende map en ga verder\">📂 next", "mt_mstop": "Stoppen met afspelen\">⏸ stop", "mt_cflac": "flac / wav omzetten naar {0}\">flac", "mt_caac": "aac / m4a omzetten naar {0}\">aac", "mt_coth": "Alle andere bestanden (geen mp3) converteren naar {0}\">oth", "mt_c2opus": "Beste keuze voor computers, laptops, android\">opus", "mt_c2owa": "opus-weba, voor iOS 17.5 en nieuwer\">owa", "mt_c2caf": "opus-caf, voor iOS 11 tot en met iOS 17\">caf", "mt_c2mp3": "Gebruik dit hele oude toestellen\">mp3", "mt_c2flac": "Beste geluidskwaliteit, maar grote downloads\">flac", //m "mt_c2wav": "Ongemprimeerde weergave (nog groter)\">wav", //m "mt_c2ok": "Mooi, goede keuze", "mt_c2nd": "Dat is niet het aanbevolen uitvoerformaat voor uw apparaat, maar dat is prima", "mt_c2ng": "Uw apparaat lijkt dit uitvoerformaat niet te ondersteunen, maar we gaan het toch proberen", "mt_xowa": "iOS bevat bugs waardoor dit formaat niet op de achtergrond kan worden afgespeeld; gebruik in plaats daarvan caf of mp3.", "mt_tint": "Achtergrond helderheid (0-100) op de zoekbalk om bufferen minder storend te maken", "mt_eq": "`Schakelt de equalizer en gain-control in;$N$Nboost `0` = standaard 100% volume (ongeweijzigd)$N$Nwidth `1  ` = standaard stereo (ongeweijzigd)$Nwidth `0.5` = 50% links-rechts crossfeed$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = stemverwijdering :^)$N$NDoor de equalizer in te schakelen, worden gapless albums volledig gapless. Laat hem dus aanstaan met alle waarden op nul (behalve width = 1) als je dat belangrijk vindt.", "mt_drc": "Schakelt de dynamic range compressor in (volume flattener / brickwaller); schakelt ook EQ in om de spaghetti te balanceren, dus zet alle EQ velden behalve ‘width’ op 0 als je dat niet wilt.$N$Nverlaagt het volume van audio boven THRESHOLD dB; voor elke RATIO dB voorbij THRESHOLD is er 1 dB output, dus standaardwaarden van tresh -24 en ratio 12 betekenen dat het nooit luider dan -22 dB zou moeten worden en het is veilig om de equalizer boost te verhogen tot 0.8, of zelfs 1.8 met ATK 0 en een enorme RLS zoals 90 (werkt alleen in firefox; RLS is max 1 in andere browsers)$N$N(zie wikipedia, die legt het veel beter uit)", "mt_ss": "`Schakelt stilte-overslaan in; vermenigvuldigt afspeelsnelheid met `vrs` nabij begin/einde wanneer volume onder `vol` is en positie binnen de eerste `beg`% of laatste `eind`%", //m "mt_ssvt": "Volumedrempel (0-255)\">vol", //m "mt_ssts": "Actieve drempel (% track, begin)\">beg", //m "mt_sste": "Actieve drempel (% track, einde)\">eind", //m "mt_sssm": "Vermenigvuldiger afspeelsnelheid\">vrs", //m "mb_play": "Afspelen", "mm_hashplay": "Deze audio bestand afspelen?", "mm_m3u": "Druk op Enter/OK om af te spelen\nDruk op ESC/Annuleren om te bewerken", "mp_breq": "Heeft firefox 82+ of chrome 73+ of iOS 15+", "mm_bload": "Aan het laden...", "mm_bconv": "Opmzetten naar {0}, even geduld...", "mm_opusen": "Uw browser kan geen aac / m4a-bestanden afspelen;\ntranscodering naar opus is nu ingeschakeld", "mm_playerr": "Afspelen mislukt: ", "mm_eabrt": "De afspeelpoging is geannuleerd", "mm_enet": "Je internetverbinding is onstabiel", "mm_edec": "Dit bestand is vermoedelijk beschadigd??", "mm_esupp": "Uw browser begrijpt deze audio-formaat niet", "mm_eunk": "Onbekende fout", "mm_e404": "Kan audio niet afspelen; fout 404: Bestand niet gevonden..", "mm_e403": "Kan audio niet afspelen; fout 403: Toegang geweigerd.\n\nProbeer op F5 te drukken om opnieuw te laden, misschien ben je uitgelogd", "mm_e415": "Kan geen audio afspelen; fout 415: Bestandsconversie mislukt; controleer serverlogs.", //m "mm_e500": "Kan geen audio afspelen; fout 500: Controleer serverlogs.", "mm_e5xx": "Kan geen audio afspelen; serverfout ", "mm_nof": "Geen audiobestanden meer vinden in de buurt", "mm_prescan": "Op zoek naar muziek om als volgende te spelen...", "mm_scank": "Het volgende nummer gevonden:", "mm_uncache": "Cache gewist; alle nummers worden opnieuw gedownload bij de volgende keer afspelen", "mm_hnf": "Dat liedje bestaat niet meer", "im_hnf": "Deze afbeelding bestaat niet meer", "f_empty": 'Deze map is leeg', "f_chide": 'Dit verbergt kolom «{0}»\n\nje kunt kolommen verbergen op de instellingen tabblad', "f_bigtxt": "Dit bestand is {0} MiB groot -- echt bekijken als tekst?", "f_bigtxt2": "Wilt u alleen het einde van het bestand bekijken? Dit maakt ook volgen/tailen mogelijk, waarbij nieuw toegevoegde tekstregels in realtime worden weergegeven.", "fbd_more": '
          {0} van de {1} bestanden weergegeven; Toon {2} of Laat alles zien
          ', "fbd_all": '
          {0} van de {1} bestanden weergegeven; Laat alles zien
          ', "f_anota": "Alleen {0} van de {1} items zijn geselecteerd;\nom de volledige map te selecteren, scrol je eerst naar beneden", "f_dls": 'de bestandslinks in de huidige map zijn veranderd in downloadlinks', "f_dl_nd": 'map wordt overgeslagen (gebruik in plaats daarvan zip/tar-download):\n', //m "f_partial": "Om een bestand dat momenteel wordt geüpload veilig te downloaden, klikt u op het bestand met dezelfde bestandsnaam, maar zonder de bestandsextensie .PARTIAL. Druk op Annuleren of Escape om dit te doen.\n\nAls u op OK / Enter drukt, wordt deze waarschuwing genegeerd en gaat u verder met het downloaden van het gedeeltelijke .PARTIAL scratchbestand, waardoor u vrijwel zeker beschadigde gegevens krijgt.", "ft_paste": "plakken {0} items$NHotkey: ctrl-V", "fr_eperm": 'kan de naam niet wijzigen:\nje hebt geen “move” rechten in deze map', "fd_eperm": 'kan niet verwijderen:\nje hebt geen “delete” rechten in deze map', "fc_eperm": 'kan niet knippen:\nje hebt geen “move” rechten in deze map', "fp_eperm": 'kan niet plakken:\nje hebt geen “schrijf” rechten in deze map', "fr_emore": "selecteer ten minste één item om te hernoemen", "fd_emore": "selecteer minstens één item om te verwijderen", "fc_emore": "selecteer ten minste één item om te knippen", "fcp_emore": "selecteer ten minste één item om naar het klembord te kopiëren", "fs_sc": "Deel de map waarin je je bevindt", "fs_ss": "De geselecteerde bestand(en) delen", "fs_just1d": "U kunt niet meer dan één map selecteren\nof mix bestanden en mappen in één selectie", "fs_abrt": "❌ Afbreken", "fs_rand": "🎲 rand.naam", "fs_go": "✅ Maak share", "fs_name": "Naam", "fs_src": "Bron", "fs_pwd": "Wachtwoord", "fs_exp": "Verloopt", "fs_tmin": "min", "fs_thrs": "uur", "fs_tdays": "dag(en)", "fs_never": "eeuwig", "fs_pname": "Optionele linknaam; is willekeurig als deze leeg is", "fs_tsrc": "Het bestand of de map die u wilt delen", "fs_ppwd": "Optioneel wachtwoord", "fs_w8": "Delen...", "fs_ok": "Druk op Enter/OK naar klembord te zetten\Druk op ESC/Annuleren om te sluiten", "frt_dec": "Kan sommige gevallen van gebroken bestandsnamen oplossen\">url-decode", "frt_rst": "Gewijzigde bestandsnamen terugzetten naar de oorspronkelijke namen\">↺ reset", "frt_abrt": "Afbreken en dit venster sluiten\">❌ Annuleren", "frb_apply": "HERNOEMEN TOEPASSEN", "fr_adv": "Batch / metadata / patroon hernoemen\">Geavanceerd", "fr_case": "Hoofdlettergevoelige regex\">case", "fr_win": "Windows-veilige namen; vervangen <>:"\\|?* met japanse tekens over de volledige breedte\">win", "fr_slash": "Vervang / met een teken waardoor er geen nieuwe mappen worden gemaakt\">geen /", "fr_re": "`Regex zoekpatroon om toe te passen op originele bestandsnamen; naar capturing groups kan worden verwezen in het onderstaande opmaakveld zoals `(1)` en `(2)` enzovoort", "fr_fmt": "`Geïnspireerd door foobar2000 :$N`(titel)` wordt vervangen door de titel van het nummer,$N`[(artiest) - ](titel)` sla [dit] gedeelte over als artiest leeg is$N`$lpad((tn),2,0)` vult tracknummer op tot 2 cijfers (0X)", "fr_pdel": "Verwijderen", "fr_pnew": "Opslaan als", "fr_pname": "Geef een naam op voor je nieuwe preset", "fr_aborted": "Afgebroken", "fr_lold": "Oude naam", "fr_lnew": "Nieuwe naam", "fr_tags": "Tags voor de geselecteerde bestanden (alleen-lezen, alleen ter referentie):", "fr_busy": "Hernoemen van {0} items...\n\n{1}", "fr_efail": "Hernoemen mislukt:\n", "fr_nchg": "{0} van de nieuwe namen zijn gewijzigd als gevolg van win
          en/of geen /\n\nOK om door te gaan met deze gewijzigde nieuwe namen?", "fd_ok": "Verwijderen OK", "fd_err": "Verwijderen mislukt:\n", "fd_none": "Er is niets verwijderd; misschien geblokkeerd door serverconfiguratie (xbd)?", "fd_busy": "{0} items verwijderen...\n\n{1}", "fd_warn1": "VERWIJDER deze {0} items?", "fd_warn2": "LAATSTE KANS! Geen manier om ongedaan te maken. Verwijderen?", "fc_ok": "Knip {0} items", "fc_warn": 'Knip {0} items\n\nmaar: alleen deze browser-tabblad kan weer plakken\n(omdat de selectie zo enorm is)', "fcc_ok": "{0} items naar klembord gekopieerd", "fcc_warn": '{0} items naar klembord gekopieerd\n\maar: alleen deze browser-tabblad kan weer plakken\n(omdat de selectie zo enorm is)', "fp_apply": "Gebruik deze namen", "fp_skip": "Conflicten overslaan", //m "fp_ecut": "Knip of kopieer eerst enkele bestanden/mappen om te verplaatsen/plakken\n\nnotitie: je kunt knippen/plakken in verschillende browsertabbladen", "fp_ename": "{0} items kunnen hier niet worden verplaatst omdat de namen al in gebruik zijn. Geef ze hieronder een nieuwe naam om verder te gaan, of verwijder de naam (\"conflicten overslaan\") om ze over te slaan:", //m "fcp_ename": "{0} items kunnen hier niet worden gekopieerd omdat de namen al in gebruik zijn. Geef ze hieronder een nieuwe naam om verder te gaan, of verwijder de naam (\"conflicten overslaan\") om ze over te slaan:", //m "fp_emore": "Er zijn nog enkele bestandsnaambotsingen die moeten worden opgelost", "fp_ok": "Verplaatsen OK", "fcp_ok": "Kopiëren OK", "fp_busy": "{0} items verplaatsen...\n\n{1}", "fcp_busy": "{0} items kopiëren...\n\n{1}", "fp_abrt": "afbreken...", //m "fp_err": "Verplaatsen mislukt:\n", "fcp_err": "Kopieëren mislukt:\n", "fp_confirm": "Verplaats deze {0} items hierheen?", "fcp_confirm": "Kopieer deze {0} items hier?", "fp_etab": 'Kan klembord van ander browsertabblad niet lezen', "fp_name": "Een bestand uploaden vanaf uw apparaat. Geef het een naam:", "fp_both_m": '
          Kies wat je wilt plakken
          Enter = Verplaatsen {0} bestanden van «{1}»\nESC = Upload {2} bestanden van je apparaat', "fcp_both_m": '
          Kies wat je wilt plakken
          Enter = Kopieer {0} bestanden van «{1}»\nESC = Upload {2} bestanden van je apparaat', "fp_both_b": 'VerplaatsUpload', "fcp_both_b": 'KopieerUpload', "mk_noname": "Voer een naam in het tekstveld aan de linkerkant voordat je verder gaat :p", "nmd_i1": "Voeg ook de gewenste extensie toe, bijvoorbeeld .md", //m "nmd_i2": "Je kunt alleen .{0}-bestanden maken omdat je geen verwijderrechten hebt", //m "tv_load": "Tekstdocument laden:\n\n{0}\n\n{1}% ({2} van de {3} MiB geladen)", "tv_xe1": "Kon tekstbestand niet laden:\n\nfout ", "tv_xe2": "404, bestand niet gevonden", "tv_lst": "Lijst met tekstbestanden in", "tvt_close": "Terugkeren naar mapweergave$NHotkey: M (of Esc)\">❌ Sluiten", "tvt_dl": "Download dit bestand$NHotkey: Y\">💾 download", "tvt_prev": "Vorig document tonen$NHotkey: i\">⬆ prev", "tvt_next": "Volgende document tonen$NHotkey: K\">⬇ next", "tvt_sel": "Selecteer bestand   ( voor knip / verplaats / verwijder / ... )$NHotkey: S\">sel", "tvt_j": "json verfraaien$NHotkey: shift-J\">j", //m "tvt_edit": "Bestand openen in teksteditor$NHotkey: E\">✏️ bewerk", "tvt_tail": "Bestand controleren op wijzigingen; nieuwe regels in realtime weergeven\">📡 volgen", "tvt_wrap": "Automatische terugloop\">↵", "tvt_atail": "Vergrendelen scroll naar onderkant van pagina\">⚓", "tvt_ctail": "Kleuren van terminals decoderen (ansi escape codes)\">🌈", "tvt_ntail": "Terugrollimiet (hoeveel tekst geladen moeten blijven)", "m3u_add1": "Nummer toegevoegd aan m3u afspeellijst", "m3u_addn": "{0} nummers toegevoegd aan m3u-afspeellijst", "m3u_clip": "m3u-afspeellijst nu gekopieerd naar klembord\n\nje moet een nieuw tekstbestand maken met de naam iets.m3u en de afspeellijst in dat document plakken; dit maakt het afspeelbaar", "gt_vau": "Laat geen video's zien, speel alleen de audio af\">🎧", "gt_msel": "Schakel bestandsselectie in; ctrl-klik op een bestand om te openen$N$N<em>indien actief: dubbelklik op een bestand / map om het te openen</em>$N$NHotkey: S\">multiselect", "gt_crop": "Gecentreerde miniaturen\">crop", "gt_3x": "Hi-res miniaturen\">3x", "gt_zoom": "Zoom", "gt_chop": "Verkorten", "gt_sort": "Sorteer bij", "gt_name": "naam", "gt_sz": "grootte", "gt_ts": "datum", "gt_ext": "type", "gt_c1": "Bestandsnamen meer inkorten (minder tonen)", "gt_c2": "Bestandsnamen minder inkorten (meer tonen)", "sm_w8": "Zoeken...", "sm_prev": "Onderstaande zoekresultaten zijn afkomstig van een eerdere zoekopdracht:\n ", "sl_close": "Zoekresultaten sluiten", "sl_hits": "Toont {0} treffers", "sl_moar": "Laad meer", "s_sz": "grootte", "s_dt": "datum", "s_rd": "pad", "s_fn": "naam", "s_ta": "tags", "s_ua": "op@", "s_ad": "adv.", "s_s1": "Minimaal MiB", "s_s2": "Maximaal MiB", "s_d1": "Min. iso8601", "s_d2": "Max. iso8601", "s_u1": "Uploaded na", "s_u2": "en/of voor", "s_r1": "Pad bevad   (spatie-gescheiden)", "s_f1": "Naam bevat   (ontkennen met -nope)", "s_t1": "Tags bevat   (^=start, einde=$)", "s_a1": "Specifieke metadata-eigenschappen", "md_eshow": "Kan niet weergeven ", "md_off": "[📜readme] uitgeschakeld in [⚙️] -- document verborgen", "badreply": "Mislukt om antwoord van server te parsen", "xhr403": "403: Toegang geweigerd\n\nprobeer F5 in te drukken, misschien ben je uitgelogd", "xhr0": "Onbekend (waarschijnlijk verbinding met server verloren of server is offline)", "cf_ok": "Sorry daarvoor -- DD" + wah + "OS-bescherming ingeschakeld\n\nalles zou binnen ongeveer 30 seconden moeten hervatten\n\nals er niets gebeurt, druk dan op F5 om de pagina opnieuw te laden", "tl_xe1": "Kon submappen niet weergeven:\n\nfout ", "tl_xe2": "404: Map niet gevonden", "fl_xe1": "Kon bestanden in map niet weergeven:\n\nfout ", "fl_xe2": "404: Map niet gevonden", "fd_xe1": "Kon submap niet aanmaken:\n\nfout ", "fd_xe2": "404: Bovenliggende map niet gevonden", "fsm_xe1": "Kon bericht niet verzenden:\n\nfout ", "fsm_xe2": "404: Bovenliggende map niet gevonden", "fu_xe1": "Mislukt om unpost lijst van server te laden:\n\nfout ", "fu_xe2": "404: Bestand niet gevonden??", "fz_tar": "gnu-tar bestand uitpakken (linux / mac)", "fz_pax": "pax-formaat tar uitpakken (trager)", "fz_targz": "gnu-tar met gzip niveau 3 compressie$N$Ndit is meestal erg langzaam, dus gebruik in plaats daarvan ongecomprimeerde tar", "fz_tarxz": "gnu-tar met xz-niveau 1 compressie$N$Ndit is meestal erg langzaam, dus gebruik in plaats daarvan ongecomprimeerde tar", "fz_zip8": "Zip met utf8 bestandsnamen (misschien onhandig op windows 7 en ouder)", "fz_zipd": "Zip met traditionele cp437-bestandsnamen, voor echt oude software", "fz_zipc": "cp437 met crc32 vroeg berekend$Nvoor MS-DOS PKZIP v2.04g (oktober 1993)$N(het duurt langer voordat het downloaden kan beginnen)", "un_m1": "Hieronder kunt u uw recente uploads verwijderen (of onvoltooide uploads afbreken)", "un_upd": "Vernieuwen", "un_m4": "of deel de bestanden die hieronder zichtbaar zijn:", "un_ulist": "Toon", "un_ucopy": "Kopieer", "un_flt": "Optionele filter:  URL moet het volgende bevatten", "un_fclr": "Reset filter", "un_derr": 'unpost-verwijderen mislukt:\n', "un_f5": 'Er is iets kapot, probeer te verversen of druk op F5', "un_uf5": "Sorry, maar u moet de pagina vernieuwen (bijvoorbeeld door op F5 of CTRL-R te drukken) voordat deze upload kan worden afgebroken.", "un_nou": 'Waarschuwing: server te druk om onvoltooide uploads weer te geven; klik straks op de "refresh" link', "un_noc": 'Waarschuwing: unpost van volledig geüploade bestanden is niet ingeschakeld/toegestaan in de serverconfiguratie', "un_max": "Toont de eerste 2000 bestanden (gebruik de filter)", "un_avail": "{0} recente uploads kunnen worden verwijderd
          {1} onvoltooide kunnen worden afgebroken", "un_m2": "Gesorteerd op uploadtijd; meest recente eerst:", "un_no1": "sike! geen enkele upload is recent genoeg", "un_no2": "sike! geen uploads die aan dat filter voldoen zijn voldoende recent", "un_next": "Verwijder de volgende {0} bestanden", "un_abrt": "Afbreken", "un_del": "Verwijderen", "un_m3": "Je recente uploads laden...", "un_busy": "Verwijderen van {0} bestanden...", "un_clip": "{0} links gekopieerd naar klembord", "u_https1": "Je moet", "u_https2": "overschakelen naar https", "u_https3": "voor betere prestaties", "u_ancient": 'Je browser is indrukwekkend oud -- misschien moet je in plaats daarvan bup gebruiken', "u_nowork": "Je moet firefox 53+ of chrome 57+ of iOS 11+ hebben", "tail_2old": "Je moet firefox 105+ of chrome 71+ of iOS 14.5+ hebben", "u_nodrop": 'Je browser is te oud voor uploaden via slepen en neerzetten', "u_notdir": "Dat is geen map!\n\nuw browser is te oud,\nprobeer in plaats daarvan sleep en neerzetten", "u_uri": "Om afbeeldingen te slepen vanuit andere browser tabblad,\nplaats deze dan op de grote uploadknop", "u_enpot": 'Overschakelen naar potato UI (kan uploadsnelheid verbeteren)', "u_depot": 'Overschakelen naar fancy UI (kan uploadsnelheid verminderen)', "u_gotpot": 'Overschakelen naar de potato UI voor verbeterde uploadsnelheid,\n\nVoel je vrij om het er niet mee eens te zijn en schakel terug!', "u_pott": "

          Bestanden:   {0} klaar,   {1} mislukt,   {2} bezig,   {3} in de wachtrij

          ", "u_ever": "Dit is de basis uploader; up2k heeft minstens het volgende nodig
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'Dit is de basis uploader; up2k is beter', "u_uput": 'Optimaliseren voor snelheid (checksum overslaan)', "u_ewrite": 'Je hebt geen schrijftoegang tot deze map', "u_eread": 'Je hebt geen leestoegang tot deze map', "u_enoi": 'Zoeken naar bestanden is niet ingeschakeld in de serverconfiguratie', "u_enoow": "Overschrijven zal hier niet werken; je heb verwijder toestemming nodig", "u_badf": 'Deze {0} bestanden (van {1} totaal) zijn overgeslagen, mogelijk door bestandssysteemmachtigingen:\n\n', "u_blankf": 'Deze {0} bestanden (van {1} totaal) zijn leeg; alsnog uploaden?\n\n', "u_applef": 'Deze {0} bestanden (van {1} totaal) zijn waarschijnlijk ongewenst;\nKlik op OK/Enter om de volgende bestanden over te slaan,\Klik op Annuleren/ESC niet uit te sluiten en deze ook te uploaden:\n\n', "u_just1": '\nMisschien werkt het beter als je slechts één bestand selecteert', "u_ff_many": "Als je Linux / MacOS / Android, gebruikt dan kan deze hoeveelheid bestanden Firefox crashen!\nals dat gebeurt, probeer het dan opnieuw (of gebruik Chrome).", "u_up_life": "Deze upload wordt verwijderd van de server\n{0} nadat het is voltooid", "u_asku": 'Upload deze {0} bestanden naar {1}', "u_unpt": "Je kunt deze upload ongedaan maken / verwijderen met de linkerbovenhoek 🧯", "u_bigtab": 'We staan op het punt om {0} bestanden te tonen\n\nDit kan uw browser laten crashen, weet je het zeker??', "u_scan": 'Bestanden scannen...', "u_dirstuck": 'Directory iterator liep vast bij het benaderen van het volgende {0} items; zal het volgende overslaan:', "u_etadone": 'Klaar ({0}, {1} bestanden)', "u_etaprep": '(klaarmaken om te uploaden)', "u_hashdone": 'hashing klaar', "u_hashing": 'Hash', "u_hs": 'Hallo zeggen...', "u_started": "De bestanden worden nu geüpload; zie [🚀]", "u_dupdefer": "Duplicaat; wordt verwerkt na alle andere bestanden", "u_actx": "klik op deze tekst om prestatieverlies
          bij het overschakelen naar andere vensters/tabbladen te voorkomen", "u_fixed": "OK!  Fixed it 👍", "u_cuerr": "Mislukt bij het uploaden van stuk {0} van {1};\nwaarschijnlijk ongevaarlijk, doorgaan\n\nbestand: {2}", "u_cuerr2": "Upload door server geweigerd (stuk {0} van {1});\nzal later opnieuw proberen\n\nbestand: {2}\n\nfout ", "u_ehstmp": "Zal opnieuw proberen; zie rechtsonder", "u_ehsfin": "Server heeft het verzoek om de upload te finaliseren afgewezen; opnieuw proberen...", "u_ehssrch": "Server heeft de zoekaanvraag afgewezen; opnieuw proberen...", "u_ehsinit": "Server heeft het verzoek om het uploaden te starten afgewezen; opnieuw proberen...", "u_eneths": "Netwerkfout tijdens het uitvoeren van de uploadhanddruk; opnieuw proberen...", "u_enethd": "Netwerkfout tijdens het testen van het bestaan van het doel; opnieuw proberen...", "u_cbusy": "Wachten tot de server ons weer vertrouwt na een netwerkstoring...", "u_ehsdf": "Server heeft geen schijfruimte meer!\n\nzal blijven proberen, voor het geval iemand genoeg ruimte vrijmaakt om door te gaan", "u_emtleak1": "Het lijkt erop dat uw webbrowser een geheugenlek heeft;\nprobeer", "u_emtleak2": ' over te schakel over naar https (aanbevolen) of ', "u_emtleak3": ' ', "u_emtleakc": 'Probeer het volgende:\n
          • druk op F5 om de pagina te verversen
          • dan schakel de  mt  uit, deze knop staat in  ⚙️ instellingen
          • en probeer de upload opnieuw
          Uploaden zal wat langzamer gaan, maar ja.\nSorry voor de problemen!\n\nPS: chrome v107 heeft een bugfix voor dit', "u_emtleakf": '{robeer het volgende:\n
          • druk op F5 om de pagina te verversen
          • dan activeer 🥔 (aardappel) in de upload scherm
          • en probeer de upload opnieuw
          \nPS: firefox heeft mogelijk een fix op een gegeven moment', "u_s404": "Niet gevonden op server", "u_expl": "Leg uit", "u_maxconn": "De meeste browsers beperken dit tot 6, maar firefox laat je dit verhogen met network.http.max-persistent-connections-per-server in about:config", "u_tu": '

          WAARSCHUWING: turbo ingeschakeld,  webbrowser detecteert en hervat onvolledige uploads mogelijk niet; zie de tooltip van de turboknop

          ', "u_ts": '

          WAARSCHUWING: turbo ingeschakeld,  zoekresultaten kunnen onjuist zijn; zie turbo-knop tooltip

          ', "u_turbo_c": "Turbo is uitgeschakeld in serverconfiguratie", "u_turbo_g": "Turbo uitgeschakeld, je geen recht om mappen in deze volume te tonen", "u_life_cfg": 'Automatisch verwijderen na minuten (of uur)', "u_life_est": 'Upload wordt verwijderd ---', "u_life_max": 'Deze map dwingt een\nmaximale levensduur van {0} af', "u_unp_ok": 'unpost is toegestaan voor {0}', "u_unp_ng": 'unpost zijn NIET toegestaan', "ue_ro": 'Je toegang tot deze map is alleen-lezen\n\n', "ue_nl": 'Je bent momenteel niet ingelogd', "ue_la": 'Je bent momenteel aangemeld als "{0}"', "ue_sr": 'U bevindt zich momenteel in de bestandszoekmodus\n\nschakel over naar uploadmodus door op het vergrootglas te klikken 🔎 (naast de grote ZOEK-knop), en probeer opnieuw te uploaden\n\nsorry', "ue_ta": 'Probeer opnieuw te uploaden, het zou nu moeten werken', "ue_ab": "Dit bestand wordt al geüpload naar een andere map en die upload moet worden voltooid voordat het bestand naar een andere map kan worden geüpload.\n\nU kunt de eerste upload afbreken en laten vergeten met de linkerbovenhoek 🧯", "ur_1uo": "OK: Bestand succesvol geüpload", "ur_auo": "OK: Alle {0} bestanden succesvol geüpload", "ur_1so": "OK: Bestand gevonden op server", "ur_aso": "OK: Alle {0} bestanden gevonden op server", "ur_1un": "Uploaden mislukt, sorry", "ur_aun": "Alle {0} uploads mislukt, sorry", "ur_1sn": "Bestand NIET gevonden op server", "ur_asn": "De {0} bestanden zijn NIET gevonden op de server", "ur_um": "Voltooid;\n{0} upload(s) OK,\n{1} upload(s) mislukt, sorry", "ur_sm": "Voltooid;\n{0} bestand(en) gevonden op de server,\n{1} bestand(en) NIET gevonden op de server", "rc_opn": "Openen", //m "rc_ply": "Afspelen", //m "rc_pla": "Afspelen als audio", //m "rc_txt": "Openen in bestandsviewer", //m "rc_md": "Openen in teksteditor", //m "rc_dl": "Downloaden", //m "rc_zip": "Downloaden als archief", //m "rc_cpl": "Link kopiëren", //m "rc_del": "Verwijderen", //m "rc_cut": "Knippen", //m "rc_cpy": "Kopiëren", //m "rc_pst": "Plakken", //m "rc_rnm": "hernoemen", //m "rc_nfo": "Nieuwe map", //m "rc_nfi": "Nieuw bestand", //m "rc_sal": "Alles selecteren", //m "rc_sin": "Selectie omkeren", //m "rc_shf": "deel deze map", //m "rc_shs": "deel selectie", //m "lang_set": "Vernieuw de pagina om de wijziging door te voeren?", "splash": { "a1": "Update", "b1": "Hallo, hoe gaat het met jou?   (Je bent niet ingelogd)", "c1": "Uitloggen", "d1": "Voorwaarde", "d2": "Toont de status van alle actieve threads", "e1": "Configuratie opnieuw laden.", "e2": "Leest configuratiebestanden opnieuw in$N(accounts, volumes, volumeschakelaars)$Nen brengt alle e2ds-volumes in kaart$N$Nopmerking: veranderingen in globale parameters$Nvereist een volledige herstart van de server", "f1": "Je kan het volgende lezen:", "g1": "Je kan naar het volgende uploaden:", "cc1": "Schakelaars en dergelijke:", "h1": "k304 uitschakelen", "i1": "k304 inschakelen", "j1": "k304 verbreekt de verbinding voor elke HTTP 304. Dit helpt tegen bepaalde proxy servers die kunnen vastlopen/plotseling stoppen met het laden van pagina's, maar het vermindert ook de prestaties aanzienlijk", "k1": "Instellingen resetten", "l1": "Inloggen:", "ls3": "inloggen", //m "lu4": "gebruikersnaam", //m "lp4": "wachtwoord", //m "lo3": "“{0}” overal afmelden", //m "lo2": "dit zal de sessie in alle browsers beëindigen", //m "m1": "Welkom terug,", "n1": "404: bestand bestaat niet  ┐( ´ -`)┌", "o1": 'of misschien heb je geen toegang? probeer een wachtwoord of ga naar startscherm', "p1": "403: toegang geweigerd  ~┻━┻", "q1": 'Probeer een wachtwoord of ga naar startscherm', "r1": "Ga naar startscherm", ".s1": "Kaart", "t1": "Actie", "u2": "Tijd sinds iemand voor het laatst naar de server schreef$N( upload / naamswijziging / ... )$N$N17d = 17 dagen$N1h23 = 1 uur 23 minuten$N4m56 = 4 minuten 56 secondes", "v1": "Verbinden", "v2": "Gebruik deze server als een lokale harde schijf", "w1": "Overschakelen naar https", "x1": "Wachtwoord wijzigen", "y1": "Jou gedeelde items", "z1": "Ontgrendel gebied:", "ta1": "Je moet eerst een nieuw wachtwoord invoeren", "ta2": "Herhaal om nieuw wachtwoord te bevestigen:", "ta3": "Typefout gevonden; probeer het opnieuw", "nop": "FOUT: Wachtwoord mag niet leeg zijn", //m "nou": "FOUT: Gebruikersnaam en/of wachtwoord mag niet leeg zijn", //m "aa1": "Inkomend:", "ab1": "Schakel nr. 304 uit", "ac1": "Schakel nr. 304 in", "ad1": "Nr. 304 stopt al het cachegebruik. Als k304 niet voldoende was, probeer dan deze. Vermenigvuldigt het dataverbruik.!", "ae1": "Uitgaand:", "af1": "Recent geüploade bestanden weergeven", "ag1": "Bekende IdP-gebruikers weergeven", } }; ================================================ FILE: copyparty/web/tl/nno.js ================================================ Ls.nno = { "tt": "Nynorsk", "cols": { "c": "handlingsknappar", "dur": "varigheit", "q": "kvalitet / bitrate", "Ac": "lydformat", "Vc": "videoformat", "Fmt": "format / innpakning", "Ahash": "lydkontrollsum", "Vhash": "videokontrollsum", "Res": "oppløysing", "T": "filtype", "aq": "lydkvalitet / bitrate", "vq": "videokvalitet / bitrate", "pixfmt": "fargekoding / detaljnivå", "resw": "horisontal oppløysing", "resh": "vertikal oppløysing", "chs": "lydkanaler", "hz": "lydoppløsing", }, "hks": [ [ "ymse", ["ESC", "lukk saker og ting"], "filbehandlar", ["G", "listevisning eller ikon"], ["T", "miniatyrbilder på/av"], ["⇧ A/D", "ikonstorleik"], ["ctrl-K", "slett valde"], ["ctrl-X", "klipp ut valde"], ["ctrl-C", "kopiér åt utklippstavle"], ["ctrl-V", "lim inn (flytt/kopiér)"], ["Y", "last ned valde"], ["F2", "endre namn på valde"], "filmarkering", ["space", "markér fil"], ["↑/↓", "flytt markør"], ["ctrl ↑/↓", "flytt markør og scroll"], ["⇧ ↑/↓", "velg forr./neste fil"], ["ctrl-A", "velg alle filer / mapper"], ], [ "navigering", ["B", "mappehierarki eller filsti"], ["I/K", "forr./neste mappe"], ["M", "eitt nivå opp (eller lukk)"], ["V", "vis mapper eller tekstfiler"], ["A/D", "panelstorleik"], ], [ "musikkspelar", ["J/L", "forr./neste song"], ["U/O", "hopp 10sek bak/fram"], ["0..9", "hopp åt 0%..90%"], ["P", "pause, eller start / fortsett"], ["S", "marker spelande song"], ["Y", "last ned song"], ], [ "bildevisar", ["J/L, ←/→", "forr./neste bilde"], ["Home/End", "første/siste bilde"], ["F", "fullskjermvisning"], ["R", "rotér åt høyre"], ["⇧ R", "rotér åt venstre"], ["S", "markér bilde"], ["Y", "last ned bilde"], ], [ "videospelar", ["U/O", "hopp 10sek bak/fram"], ["P/K/Space", "pause / fortsett"], ["C", "fortsett åt neste fil"], ["V", "gjenta avspeling"], ["M", "lyd av/på"], ["[ og ]", "gjentaksintervall"], ], [ "dokumentvisar", ["I/K", "forr./neste fil"], ["M", "lukk tekstdokument"], ["E", "redigér tekstdokument"], ["S", "markér fil (for F2/ctrl-x/...)"], ["Y", "last ned tekstfil"], ["⇧ J", "formattér json"], ] ], "m_ok": "OK", "m_ng": "Avbryt", "enable": "Aktiv", "danger": "VARSKU", "clipped": "kopiert åt utklippstavla", "ht_s1": "sekund", "ht_s2": "sekund", "ht_m1": "minutt", "ht_m2": "minutt", "ht_h1": "time", "ht_h2": "timar", "ht_d1": "dag", "ht_d2": "dagar", "ht_and": " og ", "goh": "kontrollpanel", "gop": 'navigér åt mappa før den her">forr.', "gou": 'navigér eitt nivå opp">opp', "gon": 'navigér åt mappa etter den her">neste', "logout": "Logg ut ", "login": "Logg inn", "access": " åtgang", "ot_close": "lukk reiskap", "ot_search": "`søk etter filer ved å angje filnamn, mappenamn, tid, storleik, eller metadata som songtittel / artist / osv.$N$N`foo bar` = inneheld båe «foo» og «bar»,$N`foo -bar` = innehold «foo» men ikkje «bar»,$N`^yana .opus$` = startar med «yana», filtype «opus»$N`"try unite"` = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N`2009-12-31` eller `2020-09-12 23:30:00`", "ot_unpost": "unpost: slett filer som du nyleg har lastet opp; «angre-knappen»", "ot_bup": "bup: tradisjonell / primitiv filopplasting,$N$Nfungerar i om lag samtlege nettlesarar", "ot_mkdir": "mkdir: lag ei ny mappe", "ot_md": "new-file: lag ein ny tekstfil", "ot_msg": "msg: send ein beskjed åt serverloggen", "ot_mp": "musikkspelarinstillinger", "ot_cfg": "andre innstillinger", "ot_u2i": 'up2k: last opp filer (viss du har skriveåtgang) eller bytt åt søkemodus for å sjekke om filene finnast ein eller annan plass på serveren$N$Nopplastinger kan startast opp att etter avbrot, skjer stykkevis for potensielt høgare ytelse, og ivaretek datostempling -- men bruker litt meir prosessorkraft enn [🎈]  (den primitive opplastaren "bup")

          mens opplastinger føregår så visast framdrifta her oppe!', "ot_u2w": 'up2k: filopplasting med støtte for å starte opp att avbrotne opplastinger -- steng ned nettlesaren og drage dei same filene inn i nettlesaren igjen for å plukke opp att der du slapp$N$Nopplastinger skjer stykkevis for potensielt høgare ytelse, og ivaretek datostempling -- men bruker litt meir prosessorkraft enn [🎈]  (den primitive opplastaren "bup")

          mens opplastinger føregår så visast framdrifta her oppe!', "ot_noie": 'Fungerer mye betre i Chrome / Firefox / Edge', "ab_mkdir": "lag mappe", "ab_mkdoc": "ny tekstfil", "ab_msg": "send melding", "ay_path": "gå videre åt mapper", "ay_files": "gå videre åt filer", "wt_ren": "gje nye namn åt dei valde filene$NSnarvei: F2", "wt_del": "slett dei valde filene$NSnarvei: ctrl-K", "wt_cut": "klipp ut dei valde filene <small>(for å lime inn ein annan plass)</small>$NSnarvei: ctrl-X", "wt_cpy": "kopiér dei valde filene åt utklippstavla$N(for å lime inn ein annan plass)$NSnarvei: ctrl-C", "wt_pst": "lim inn filer (som tidligare blei klipt ut / kopiert ein annan plass)$NSnarvei: ctrl-V", "wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på ei fil)", "wt_selinv": "invertér utval", "wt_zip1": "last ned denne mappa som eit arkiv", "wt_selzip": "last ned dei valde filene som eit arkiv", "wt_seldl": "last ned dei valde filene$NSnarvei: Y", "wt_npirc": "kopiér songinfo (irc-formatert)", "wt_nptxt": "kopiér songinfo", "wt_m3ua": "legg song åt i m3u-speleliste$N(husk å klikk på 📻copy senere)", "wt_m3uc": "kopiér m3u-spelelista åt utklippstavla", "wt_grid": "bytt mellom ikon og listevising$NSnarvei: G", "wt_prev": "førre song$NSnarvei: J", "wt_play": "play / pause$NSnarvei: P", "wt_next": "neste song$NSnarvei: L", "ul_par": "samtidige handl.:", "ut_rand": "finn opp nye tilfeldige filnamn", "ut_u2ts": "gje fila på serveren same$Ntidsstempel som lokalt hos deg\">📅", "ut_ow": "overskrive eksisterande filer på serveren?$N🛡️: aldri (finn på eit nytt filnamn i staden for)$N🕒: overskriv viss fila åt serveren er eldre$N♻️: alltid, gitt at innhaldet er annleis$N⏭️: hopp over alle eksisterande filer", "ut_mt": "fortsett å synfare køa mens opplasting føregår$N$Nskru denne av dersom du har ein$Ntreig prosessor eller harddisk", "ut_ask": 'bekreft filutvalg før opplasting startar">💭', "ut_pot": "forbetre ytinga på treige einheiter ved å$Nforenkle brukergrensesnittet", "ut_srch": "gjer eit søk i staden for å laste opp --$Nleitar gjennom alle mappane du har lov åt å sjå", "ut_par": "sett åt 0 for å midlertidig stoppe opplasting$N$Nhøge verdier (4 eller 8) kan gje betre yting,$Nspesielt på treige internettlinjer$N$Nbør ikkje vere høgare enn 1 på LAN$Neller viss serveren sin harddisk er treig", "ul_btn": "slepp filer / mapper
          her (eller klikk meg)", "ul_btnu": "L A S T   O P P", "ul_btns": "F I L S Ø K", "ul_hash": "synfar", "ul_send": " send", "ul_done": "total", "ul_idle1": "ingen handlinger i køen", "ut_etah": "snitthastigheit for <em>synfaring</em> samt gjenståande tid", "ut_etau": "snitthastigheit for <em>opplasting</em> samt gjenståande tid", "ut_etat": "<em>total</em> snitthastigheit og gjenståande tid", "uct_ok": "fullført uten problem", "uct_ng": "fullført under tvil (duplikat, ikkje funne, ...)", "uct_done": "fullført (enten <em>ok</em> eller <em>ng</em>)", "uct_bz": "aktive handlinger (synfaring / opplasting)", "uct_q": "køa", "utl_name": "filnamn", "utl_ulist": "vis", "utl_ucopy": "kopiér", "utl_links": "lenker", "utl_stat": "status", "utl_prog": "fremdrift", // må vere korte: "utl_404": "404", "utl_err": "FEIL!", "utl_oserr": "OS-feil", "utl_found": "funnet", "utl_defer": "seinare", "utl_yolo": "YOLO", "utl_done": "ferdig", "ul_flagblk": "filene har blitt lagd i køa
          men det er ein anna nettlesarfane som held på med synfaring eller opplasting akkurat no,
          så venter åt den er ferdig først", "ul_btnlk": "brytaren har blitt låst åt denne tilstanden i serverens konfigurasjon", "udt_up": "Last opp", "udt_srch": "Søk", "udt_drop": "Slepp filene her", "u_nav_m": '
          kva har du?
          Enter = Filer (éin eller fleire)\nESC = Éi mappe (inkludert undermapper)', "u_nav_b": 'FilerÉi mappe', "cl_opts": "brytarar", "cl_hfsz": "filstorleik", "cl_themes": "utsjånad", "cl_langs": "språk", "cl_ziptype": "nedlasting av mapper", "cl_uopts": "up2k-brytarar", "cl_favico": "favicon", "cl_bigdir": "store mapper", "cl_hsort": "#sort", "cl_keytype": "notasjon for musikalsk dur", "cl_hiddenc": "skjulte kolonner", "cl_hidec": "skjul", "cl_reset": "nullstill", "cl_hpick": "klikk på overskrifta åt kolonnene du ønskjer å skjule i tabellen nedanfor", "cl_hcancel": "kolonne-skjuling avbrote", "cl_rcm": "høgreklikkmeny", "ct_grid": '田 ikon', "ct_ttips": 'vis hjelpetekst ved å holde musa over ting">ℹ️ tips', "ct_thumb": 'vis miniatyrbilder i staden for ikon$NSnarvei: T">🖼️ bilder', "ct_csel": 'bruk tastane CTRL og SHIFT for markering av filer i ikonvising">merk', "ct_dsel": 'klikk-og-dra for å merke filer i ikonvising">dra', "ct_dl": 'last ned filer (ikkje vis i nettleseren)">dl', "ct_ihop": 'bla ned åt sist viste bilde når bildevisaren lukkast">g⮯', "ct_dots": 'vis skjulte filer (gitt at serveren tillèt det)">.synlig', "ct_qdel": 'sletteknappen spør berre éin gong om stadfesting">hurtig🗑️', "ct_dir1st": 'sortér slik at mapper kjem framanfor filer">📁 først', "ct_nsort": 'naturlig sortering (skjønar tal i filnamn)">nsort', "ct_utc": 'bruk UTC for alle klokkeslett">UTC', "ct_readme": 'vis README.md nedanfor filene">📜 readme', "ct_idxh": 'vis index.html i staden for filliste">htm', "ct_sbars": 'vis rullgardiner / skrollefelt">⟊', "cut_umod": 'i tilfelle ei fil du lastar opp alt finnast på serveren, så skal tidsstempelet åt serveren oppdaterast slik at det stemmer overeins med din lokale fil (krev rettigheitene write+delete)">re📅', "cut_turbo": "forenkla synfaring ved opplasting; bør etter alt å døme ikkje skruast på:$N$Nnyttig dersom du var midt i ei svær opplasting som måtte startast på nytt av ein eller annan grunn, og du vil komme i gang igjen så raskt som i det heile mulig.$N$Nnår denne er skrudd på så forenklast synfaringa kraftig; i staden for å utføre ein trygg sjekk på om filene finnast på serveren i god stand, så sjekkast det kun om filstorleiken stemmer. Så dersom ein korrupt fil vere på serveren allerede, på same plass, med same storleik og namn, så blir det ikkje oppdaga.$N$Ndet anbefalast å kun benytte denne funksjonen for å komme seg raskt gjennom sjølve opplastinga, for så å skru den av, og åt slutt "laste opp" dei same filene éin gong åt -- slik at integriteten kan verifiserast\">turbo", "cut_datechk": "har ingen effekt dersom turbo er skrudd av$N$Ngjer turbo bittelitt tryggare ved å sjekke datostemplinga på filene (i tillegg åt filstorleik)$N$Nburde oppdage og gjenoppta dei fleste ufullstendige opplastinger, men er ikkje ein fullverdig erstatning for å deaktivere turbo og gjere ein skikkeleg sjekk\">date-chk", "cut_u2sz": "storleik i megabyte for kvart bruddstykke for opplasting. Store verdiar flyg betre over atlanteren. Små verdiar kan vere betre på flettande ustabile samband", "cut_flag": "samkøyrer nettlesarfaner slik at berre éin $N kan holde på med synfaring / opplasting $N -- andre faner må óg ha denne skrudd på $N -- fungerar kun innanom same domene", "cut_az": "last opp filer i alfabetisk rekkefølge, i staden for minste-fil-først$N$Nalfabetisk kan gjere det lettare å anslå om alt gjekk bra, men er bittelitt treigare på fiber / LAN", "cut_nag": "meldingsvarsel når opplasting er ferdig$N(kun om nettlesarfana ikkje er synlig)", "cut_sfx": "lydvarsel når opplasting er ferdig$N(kun om nettlesarfanen ikkje er synlig)", "cut_mt": "raskere synfaring ved å bruke heile CPU'en$N$Ndenne funksjonen nytter web-workers$Nog krev meir RAM (opptil 512 MiB ekstra)$N$Ngjer https 30% raskare, http 4.5x raskare\">mt", "cut_wasm": "bruk wasm i staden for nettlesaren sin sha512-funksjon; gjev betre yting på chrome-baserte nettlesarar, men brukar meir CPU, og eldre versjoner av chrome toler det ikkje (et opp all RAM og kræsjer)\">wasm", "cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)", "cft_fg": "farge", "cft_bg": "bakgrunnsfarge", "cdt_lim": "maks mengd filer å vise per mappe", "cdt_ask": "vis knappar for å laste fleire filer nederst på sida i staden for å gradvis laste meir av mappea når man scroller ned", "cdt_hsort": "`antall sorteringsreglar (`,sorthref`) som skal inkluderast når media-URL'ar genererast. Dersom denne er 0 så vil sorteringsreglar i URL'ar korkje bli generert eller lest", "cdt_ren": "slå på tilpassa høgreklikkmeny (den vanlege menyen er tilgjengeleg med shift + høgreklikk)\">aktiv", "cdt_rdb": "høgreklikk to gonger for å vise den vanlege høgreklikkmenyen\">x2", "tt_entree": "bytt åt mappehierarki$NSnarvei: B", "tt_detree": "bytt åt tradisjonell stivising$NSnarvei: B", "tt_visdir": "bla ned åt den åpne mappa", "tt_ftree": "bytt mellom filstruktur og tekstfiler$NSnarvei: V", "tt_pdock": "vis dei overordna mappane i eit panel", "tt_dynt": "øk bredda på panelet ettersom treet utvider seg", "tt_wrap": "linjebryting", "tt_hover": "vis heile mappenamnet når musepeikaren treff mappa$N( gjer diverre at scrollhjulet fusker dersom musepeikaren ikkje finn seg i grøfta )", "ml_pmode": "ved enden av mappa", "ml_btns": "knapper", "ml_tcode": "konvertering", "ml_tcode2": "konvertér til", "ml_tint": "tint", "ml_eq": "audio equalizer (tonejustering)", "ml_drc": "compressor (volumutjevning)", "ml_ss": "spol forbi stillheit", "mt_loop": "spel den same songen om og om igjen\">🔁", "mt_one": "spel kun éin song\">1️⃣", "mt_shuf": "songane i kvar mappe$Nspelast i tilfeldig rekkefølge\">🔀", "mt_aplay": "prøv å starte avspeling viss linken du trykte på for å åpne nettsida inneheld ein song-ID$N$Nviss denne deaktiverast så vil heller ikkje nettside-URL'en bli oppdatert med song-ID'er når musikk spelast, i tilfelle innstillingane skulle gå tapt og nettsida lastast på ny\">a▶", "mt_preload": "hent ned litt av neste song i forkant,$Nslik at pausa i overgangen blir mindre\">forsyn", "mt_prescan": "ved behov, bla åt neste mappe$Nslik at nettlesaren lar oss$Nfortsetja å spele musikk\">bla", "mt_fullpre": "hent ned heile neste song, ikkje berre litt:$N✅ skru på viss nettet ditt er ustabilt,$N❌ skru av viss nettet ditt er treigt\">full", "mt_fau": "for telefoner: forhindre at avspeling stoppar viss nettet er for treigt åt å laste neste song i tide. Viss påskrudd kan det forårsake at songinfo ikkje visast korrekt i OS'et\">☕️", "mt_waves": "waveform seekbar:$Nvis volumkurve i avspelingsfeltet\">~s", "mt_npclip": "vis knappar for å kopiere info om songen du høyrer på\">/np", "mt_m3u_c": "vis knapper for å kopiere dei valde$Nsongene som innslag i ei m3u8-speleliste\">📻", "mt_octl": "integrering med operativsystemet (fjernkontroll, infoskjerm)\">os-ctl", "mt_oseek": "gje løyve åt spoling med fjernkontroll$N$Nmerk: på nokon eininger (iPhones) så vil$Ndette erstatte knappen for neste song\">spoling", "mt_oscv": "vis albumcover på infoskjermen\">bilde", "mt_follow": "bla slik at songen som spelast alltid er synleg\">🎯", "mt_compact": "tettpakka spelarpanel\">⟎", "mt_uncache": "prøv denne viss ein song ikkje spelar riktig\">oppfrisk", "mt_mloop": "repetér heile mappa\">🔁 gjenta", "mt_mnext": "hopp åt neste mappe og fortsett\">📂 neste", "mt_mstop": "stopp avspeling\">⏸ stopp", "mt_cflac": "konvertér flac / wav-filer åt {0}\">flac", "mt_caac": "konvertér aac / m4a-filer åt to {0}\">aac", "mt_coth": "konvertér alt anna (men ikkje mp3) åt {0}\">andre", "mt_c2opus": "det beste valget for alle PCar og Android\">opus", "mt_c2owa": "opus-weba, for iOS 17.5 og nyare\">owa", "mt_c2caf": "opus-caf, for iOS 11 åt og med 17\">caf", "mt_c2mp3": "bra valg for steinalder-utstyr (slår aldri feil)\">mp3", "mt_c2flac": "gir best lydkvalitet, men et nettet ditt\">flac", "mt_c2wav": "heilt rå lydstrøm (bruker enda meir data enn flac)\">wav", "mt_c2ok": "bra valg!", "mt_c2nd": "ikkje det føretrekte valget for din einheit, men funker sikkert greit", "mt_c2ng": "ser verkelig ikkje ut som enheiten din taklar dette formatet... men ok, vi prøver", "mt_xowa": "iOS har fortsatt problem med avspeling av owa-musikk i bakgrunnen. Bruk caf eller mp3 i staden for", "mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjer oppdateringer mindre distraherande", "mt_eq": "`aktivér tonekontroll og forsterker;$N$Nboost `0` = normal volumskala$N$Nwidth `1  ` = normal stereo$Nwidth `0.5` = 50% blanding venstre-høgre$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = instrumental :^)$N$Nreduserer óg daudtid mellom songfiler", "mt_drc": "aktivér volum-utjevning (dynamic range compressor); vil óg aktivere tonejustering, så sett alle EQ-feltene bortsett frå 'width' åt 0 viss du ikkje vil ha nokon EQ$N$Nfilteret vil dempe volumet på alt som er høgare enn TRESH dB; for kvar RATIO dB over grensa er det 1dB som treff høgtalarane, så standardverdiane tresh -24 og ratio 12 skal bety at volumet ikkje gjeng høgare enn -22 dB, slik at ein trygt kan øke boost-verdien i equalizeren åt rundt 0.8, eller 1.8 kombinert med ATK 0 og RLS 90 (berre mulig i firefox; andre nettlesarar tek ikkje høgare RLS enn 1)$N$Nwikipedia forklarar dette mykje betre forresten", "mt_ss": "`spolar forbi stille parti i songar; spelar `ffwd` gongar raskare nær starten/slutten av songen når volumet er under `volum` og posisjonen er innanfor dei første `start`% eller dei siste `slutt`% av songen", "mt_ssvt": "volumterskel (0-255)\">volum", "mt_ssts": "aktiv innanfor første % av songen\">start", "mt_sste": "aktiv innanfor siste % av songen\">slutt", "mt_sssm": "avspelingshastigheitsmultiplikator\">ffwd", "mb_play": "lytt", "mm_hashplay": "spel denne songen?", "mm_m3u": "trykk Enter/OK for å spele\ntrykk ESC/Avbryt for å redigere", "mp_breq": "krev firefox 82+, chrome 73+, eller iOS 15+", "mm_bload": "lastar inn...", "mm_bconv": "konverterer åt {0}, vent litt...", "mm_opusen": "nettlesaren din skjønar ikkje aac / m4a;\nkonvertering åt opus er no aktivert", "mm_playerr": "avspeling feilet: ", "mm_eabrt": "Avspelingsforespørselen blei avbroten", "mm_enet": "Nettet ditt er ustabilt", "mm_edec": "Noko er galt med musikkfila", "mm_esupp": "Nettleseren din skjønar ikkje filtypen", "mm_eunk": "Ukjent feil", "mm_e404": "Avspeling feilet: Fil ikkje funnet.", "mm_e403": "Avspeling feilet: Høve nekta.\n\nKanskje du blei logget ut?\nPrøv å trykk F5 for å laste sida på nytt.", "mm_e415": "Avspeling feilet: Kunne ikkje konvertere fila, sjekk serverloggen.", "mm_e500": "Avspeling feilet: Rusk i maskineriet, sjekk serverloggen.", "mm_e5xx": "Avspeling feilet: ", "mm_nof": "finn ikkje flere songer i nærheita", "mm_prescan": "Leitar etter neste song...", "mm_scank": "Fann neste song:", "mm_uncache": "alle songer vil lastast på nytt ved neste avspeling", "mm_hnf": "songen finnast ikkje lenger", "im_hnf": "bildet finnast ikkje lenger", "f_empty": 'denne mappa er tom', "f_chide": 'dette vil skjule kolonna «{0}»\n\nfana for "andre innstillinger" let deg vise kolonna igjen', "f_bigtxt": "denne fila er heeile {0} MiB -- vis som tekst?", "f_bigtxt2": "vil du sjå bunnen av filen i staden for? du vil da óg sjå nye linjer som blir lagd åt på slutten av filen i sanntid", "fbd_more": '
          visar {0} av {1} filer; vis {2} eller vis alle
          ', "fbd_all": '
          visar {0} av {1} filer; vis alle
          ', "f_anota": "kun {0} av totalt {1} element blei markert;\nfor å velje alt må du bla åt bunnen av mappa først", "f_dls": 'lenkane i denne mappa er no\nomgjort åt nedlastingsknappar', "f_dl_nd": 'hoppar over mappe (bruk zip/tar-nedlasting i staden):\n', "f_partial": "For å laste ned ei fil som enda ikkje er ferdig opplasta, klikk på filen som har same filnamn som denne, men uten .PARTIAL på slutten. Da vil serveren passe på at nedlastinga går bra. Derfor anbefalast det sterkt å trykkje AVBRYT eller Escape-tasten.\n\nViss du verkelig ønskjer å laste ned denne .PARTIAL-filen på ein ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høgst sannsynleg motta korrupt data.", "ft_paste": "Lim inn {0} filer$NSnarvei: ctrl-V", "fr_eperm": 'kan ikkje endre namn:\ndu har ikkje høve åt “move” i denne mappa', "fd_eperm": 'kan ikkje slette:\ndu har ikkje høve åt “delete” i denne mappa', "fc_eperm": 'kan ikkje klippe ut:\ndu har ikkje høve åt “move” i denne mappa', "fp_eperm": 'kan ikkje lime inn:\ndu har ikkje høve åt “write” i denne mappa', "fr_emore": "vel minst éi fil som skal få nytt namn", "fd_emore": "vel minst éi fil som skal slettast", "fc_emore": "vel minst éi fil som skal klippast ut", "fcp_emore": "vel minst éi fil som skal kopierast åt utklippstavla", "fs_sc": "del mappa du er i no", "fs_ss": "del dei valde filene", "fs_just1d": "du kan ikkje markere flere mapper samtidig,\neller kombinere mapper og filer", "fs_abrt": "❌ avbryt", "fs_rand": "🎲 tilfeldig namn", "fs_go": "✅ opprett deling", "fs_name": "namn", "fs_src": "kjelde", "fs_pwd": "passord", "fs_exp": "varigheit", "fs_tmin": "min", "fs_thrs": "timar", "fs_tdays": "dagar", "fs_never": "for evig", "fs_pname": "valfri namn (blir litt tilfeldig ellers)", "fs_tsrc": "fil/mappe som skal delast", "fs_ppwd": "valfri passord", "fs_w8": "opprettar deling...", "fs_ok": "trykk Enter/OK for å kopiere lenka (for CTRL-V)\ntrykk ESC/Avbryt for å kun bekrefta", "frt_dec": "kan korrigere visse ødelagte filnamn\">url-decode", "frt_rst": "nullstillar endringar (tilbake åt dei originale filnamna)\">↺ reset", "frt_abrt": "avbryt og lukk dette vindauget\">❌ avbryt", "frb_apply": "IVERKSETT", "fr_adv": "automasjon basert på metadata
          og / eller mønster (regulære uttrykk)\">avansert", "fr_case": "versalfølsomme uttrykk\">Aa", "fr_win": "bytt ut bokstavane <>:"\\|?* med$Ntilsvarande som windows ikkje får panikk av\">win", "fr_slash": "bytt ut bokstaven / slik at den ikkje forårsakar at nye mapper opprettes\">ikke /", "fr_re": "`regex-mønster som køyrast på kvart filnamn. Grupper kan leses ut i format-feltet nedanfor, f.eks. `(1)` og `(2)` osv.", "fr_fmt": "`inspirert av foobar2000:$N`(title)` byttast ut med songtittel,$N`[(artist) - ](title)` dropper [dette] viss artist er blank$N`$lpad((tn),2,0)` visar songnr. med 2 siffer", "fr_pdel": "slett", "fr_pnew": "lagre som", "fr_pname": "gje innstillingane dine eit namn", "fr_aborted": "avbrote", "fr_lold": "gamalt namn", "fr_lnew": "nytt namn", "fr_tags": "metadata for dei valde filene (kun for referanse):", "fr_busy": "endrar namn på {0} filer...\n\n{1}", "fr_efail": "endring av namn feila:\n", "fr_nchg": "{0} av namna blei justert pga. win og/eller ikkje /\n\nvil du fortsetja med dei nye namna som blei valde?", "fd_ok": "sletting OK", "fd_err": "sletting feila:\n", "fd_none": "ingenting blei sletta; kanskje avvist av serverkonfigurasjon (xbd)?", "fd_busy": "slettar {0} filer...\n\n{1}", "fd_warn1": "SLETT disse {0} filene?", "fd_warn2": "Siste sjanse! Dette kan ikkje angrast. Slett?", "fc_ok": "klipte ut {0} filer", "fc_warn": 'klipte ut {0} filer\n\nmen: kun denne nettlesarfana har muligheit åt å lime dei inn ein annan plass, siden antallet filer er helt hinsides', "fcc_ok": "kopierte {0} filer åt utklippstavla", "fcc_warn": 'kopierte {0} filer åt utklippstavla\n\nmen: kun denne nettlesarfana har muligheit åt å lime dei inn ein annan plass, sidan antallet filer er heilt på hi sida', "fp_apply": "bekreft og lim inn no", "fp_skip": "berre ledige", "fp_ecut": "du må klippe ut eller kopiere nokre filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettlesarfaner; klippe ut i éi fane, lime inn i ei anna", "fp_ename": "{0} filer kan ikkje flyttast åt målmappa fordi det allereie finnast filer med same namn. Gi dei nye namn nedanfor, eller gje dei eit blankt namn (\"berre ledige\") for å hoppe over dei:", "fcp_ename": "{0} filer kan ikkje kopierast åt målmappa fordi det allereie finnast filer med same namn. Gi dei nye namn nedanfor, eller gje dei eit blankt namn (\"berre ledige\") for å hoppe over dei:", "fp_emore": "det er fortsatt fleire namn som må endrast", "fp_ok": "flytting OK", "fcp_ok": "kopiering OK", "fp_busy": "flyttar {0} filer...\n\n{1}", "fcp_busy": "kopierar {0} filer...\n\n{1}", "fp_abrt": "avbryt...", "fp_err": "flytting feila:\n", "fcp_err": "kopiering feila:\n", "fp_confirm": "flytt disse {0} filene hit?", "fcp_confirm": "kopiér disse {0} filene hit?", "fp_etab": 'kunne ikkje lese lista med filer frå den andre nettlesarfana', "fp_name": "Lastar opp éi fil frå einheita di. Velg filnamn:", "fp_both_m": '
          kva skal limast inn her?
          Enter = Flytt {0} filer frå «{1}»\nESC = Last opp {2} filer frå einheita din', "fcp_both_m": '
          kva skal limes inn her?
          Enter = Kopiér {0} filer frå «{1}»\nESC = Last opp {2} filer frå einheita din', "fp_both_b": 'FlyttLast opp', "fcp_both_b": 'KopiérLast opp', "mk_noname": "skriv inn eit namn i tekstboksa åt venstre først :p", "nmd_i1": "leggja også til filendinga du vil, til dømes .md", "nmd_i2": "du kan berre laga .{0}-filer fordi du ikkje har delete-tilgang", "tv_load": "Lastar inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lasta ned)", "tv_xe1": "kunne ikkje laste tekstfil:\n\nfeil ", "tv_xe2": "404, Fil ikkje funne", "tv_lst": "tekstfiler i mappa", "tvt_close": "gå tilbake åt mappa$NSnarvei: M (eller Esc)\">❌ lukk", "tvt_dl": "last ned denne fila$NSnarvei: Y\">💾 last ned", "tvt_prev": "vis førre dokument$NSnarvei: i\">⬆ forr.", "tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste", "tvt_sel": "markér fila   ( for utklipp / sletting / ... )$NSnarvei: S\">merk", "tvt_j": "formattér json$NSnarvei: shift-J\">j", "tvt_edit": "redigér fila$NSnarvei: E\">✏️ endre", "tvt_tail": "overvak fila for endringar og vis nye linjer i sanntid\">📡 følg", "tvt_wrap": "tekstbryting\">↵", "tvt_atail": "hald dei nyaste linjene synlege (lås åt botnen av sida)\">⚓", "tvt_ctail": "skjøn og vis terminalfargar (ansi-sekvensar)\">🌈", "tvt_ntail": "maksgrense for antal bokstavar som skal visast i vindauget", "m3u_add1": "songen blei lagd åt i m3u-spelelista", "m3u_addn": "{0} songer blei lagde åt i m3u-spelelista", "m3u_clip": "m3u-spelelista blei kopiert åt utklippstavla\n\nneste steg er å oppretta eit tekstdokument med filnamn som sluttar på .m3u og lime inn spelelista der", "gt_vau": "ikkje vis videofiler, berre spel lyden\">🎧", "gt_msel": "markér filer i staden for å åpne dei; ctrl-klikk filer for å overstyre$N$N<em>når aktiv: dobbelklikk ei fil / mappe for å åpne</em>$N$NSnarvei: S\">markering", "gt_crop": "skjer ikona slik at dei passar betre\">✂", "gt_3x": "høgare oppløysing på ikon\">3x", "gt_zoom": "zoom", "gt_chop": "trim", "gt_sort": "sortér", "gt_name": "namn", "gt_sz": "størr.", "gt_ts": "dato", "gt_ext": "type", "gt_c1": "redusér makslengde på filnamn", "gt_c2": "auk makslengde på filnamn", "sm_w8": "søker...", "sm_prev": "søkeresultata er frå eit tidlegare søk:\n ", "sl_close": "lukk søkeresultat", "sl_hits": "visar {0} treff", "sl_moar": "hent fleire", "s_sz": "størr.", "s_dt": "dato", "s_rd": "sti", "s_fn": "namn", "s_ta": "meta", "s_ua": "up@", "s_ad": "avns.", "s_s1": "større enn ↓ MiB", "s_s2": "mindre enn ↓ MiB", "s_d1": "nyare enn <dato>", "s_d2": "eldre enn", "s_u1": "lasta opp etter", "s_u2": "og/eller før", "s_r1": "mappaamn inneheld", "s_f1": "filnamn inneheld", "s_t1": "song-info inneheld", "s_a1": "konkrete eigenskapar", "md_eshow": "visar forenkla ", "md_off": "[📜readme] er skrudd av i [⚙️] -- dokument skjult", "badreply": "Ugyldig svar frå serveren", "xhr403": "403: Høve nekta\n\nkanskje du blei logga ut? prøv å trykk F5", "xhr0": "ukjend (enten nettverksproblem eller serverkræsj)", "cf_ok": "om orsak -- liten tilfeldig kontroll, alt OK\n\nting skal fortsetja om ca. 30 sekund\n\nviss ikkje noko skjer, trykk F5 for å laste sida på nytt", "tl_xe1": "kunne ikkje hente undermapper:\n\nfeil ", "tl_xe2": "404: Mappa finnast ikkje", "fl_xe1": "kunne ikkje hente filer i mappa:\n\nfeil ", "fl_xe2": "404: Mappa finnast ikkje", "fd_xe1": "kan ikkje opprette ny mappe:\n\nfeil ", "fd_xe2": "404: Den overordna mappa finnast ikkje", "fsm_xe1": "kunne ikkje sende melding:\n\nfeil ", "fsm_xe2": "404: Den overordna mappa finnast ikkje", "fu_xe1": "kunne ikkje hente lista med nyleg opplastede filer frå serveren:\n\nfeil ", "fu_xe2": "404: Fila finnast ikkje??", "fz_tar": "ukomprimert gnu-tar arkiv, for linux og mac", "fz_pax": "ukomprimert pax-tar arkiv, litt treigare", "fz_targz": "gnu-tar pakket med gzip (nivå 3)$N$NNB: denne er veldig treig;$Nukomprimert tar er betre", "fz_tarxz": "gnu-tar pakket med xz (nivå 1)$N$NNB: denne er veldig treig;$Nukomprimert tar er betre", "fz_zip8": "zip med filnamn i utf8 (noko problematisk på windows 7 og eldre)", "fz_zipd": "zip med filnamn i cp437, for høggamle maskiner", "fz_zipc": "cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(øker behandlingstid på server)", "un_m1": "nedanfor kan du angre / slette filer som du nyleg har lastet opp, eller avbryte ufullstendige opplastinger", "un_upd": "oppdater", "un_m4": "eller viss du vil dele nedlastings-lenkene:", "un_ulist": "vis", "un_ucopy": "kopiér", "un_flt": "valgfritt filter:  filnamn / filsti må inneholde", "un_fclr": "nullstill filter", "un_derr": 'unpost-sletting feilet:\n', "un_f5": 'noko gjekk galt, prøv å oppdatere lista eller trykk F5', "un_uf5": "om orsak, men du må laste sida på nytt (f.eks. ved å trykkje F5 eller CTRL-R) før denne opplastinga kan avbrytast", "un_nou": 'advarsel: kan ikkje vise ufullstendige opplastingar akkurat no; klikk på oppdater-lenka om litt', "un_noc": 'advarsel: angring av fullførte opplastingar er deaktivert i serverkonfigurasjonen', "un_max": "visar dei første 2000 filene (bruk filteret for å snevre inn)", "un_avail": "{0} nyleg opplasta filer kan slettast
          {1} ufullstendige opplastingar kan avbrytast", "un_m2": "sortert etter opplastingstid; nyaste først:", "un_no1": "men nei, her var det jaggu ikkje noko som slettast kan", "un_no2": "men nei, her var det jaggu ingenting som passa overens med filteret", "un_next": "slett dei neste {0} filene nedanfor", "un_abrt": "avbryt", "un_del": "slett", "un_m3": "hentar lista med nyleg opplasta filer...", "un_busy": "slettar {0} filer...", "un_clip": "{0} lenkar kopiert åt utklippstavla", "u_https1": "du burde", "u_https2": "bytte åt https", "u_https3": "for høgare hastigheit", "u_ancient": 'nettlesaren din er prehistorisk -- mulig du burde bruke bup i staden for', "u_nowork": "krev firefox 53+, chrome 57+, eller iOS 11+", "tail_2old": "krev firefox 105+, chrome 71+, eller iOS 14.5+", "u_nodrop": 'nettlesaren din er for gamal åt å laste opp filer ved å drage dei inn i vindauget', "u_notdir": "mottok ikkje mappa!\n\nnettlesaren din er for gamal,\nprøv å drage mappa inn i vindauget i staden for", "u_uri": "for å laste opp bilder frå andre nettlesarvindauge,\nslipp bildet rett på den store last-opp-knappen", "u_enpot": 'bytt åt enkelt UI (gir sannsynleg raskere opplasting)', "u_depot": 'bytt åt snæsent UI (gir sannsynleg treigare opplasting)', "u_gotpot": 'bytta åt eit enklare UI for å laste opp raskere,\n\ndu kan gjerne bytte tilbake altså!', "u_pott": "

          filer:   {0} ferdig,   {1} feilet,   {2} behandlast,   {3} i kø

          ", "u_ever": "dette er den primitive opplastaren; up2k krev minst:
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'dette er den primitive opplastaren; up2k er betre', "u_uput": 'litt raskare (uten sha512)', "u_ewrite": 'du har ikkje høve til å skrive i denne mappa', "u_eread": 'du har ikkje høve til å lese i denne mappa', "u_enoi": 'filsøk er deaktivert i serverkonfigurasjonen', "u_enoow": "kan ikkje overskrive filer her (Delete-rettigheiten er nødvendig)", "u_badf": 'Disse {0} filene (av totalt {1}) kan ikkje leses, kanskje pga rettigheitsproblem i filsystemet på datamaskinen din:\n\n', "u_blankf": 'Disse {0} filene (av totalt {1}) er blanke / uten innhald; ønskjer du å laste dei opp uansett?\n\n', "u_applef": 'Disse {0} filene (av totalt {1}) er antakeleg uønska;\nTrykk OK/Enter for å HOPPE OVER disse filene,\nTrykk Avbryt/ESC for å LASTE OPP disse filene óg:\n\n', "u_just1": '\nFunkar kanskje betre viss du berre tar éi fil om gangen', "u_ff_many": 'Viss du bruker Linux / MacOS / Android, så kan dette antalet filer
          kanskje kræsje Firefox! Viss det skjer, så prøv igjen (eller bruk Chrome).', "u_up_life": "Filene slettast frå serveren {0}\netter at opplastingen er fullført", "u_asku": 'Laste opp disse {0} filene åt {1}', "u_unpt": "Du kan angre / slette opplastinga med 🧯 oppe åt venstre", "u_bigtab": 'Vil no vise {0} filer...\n\nDette kan kræsje nettlesaren din. Fortsette?', "u_scan": 'Les mappane...', "u_dirstuck": 'Nettleseren din fekk ikkje høve åt å lese følgande {0} filer/mapper, så dei blir hoppa over:', "u_etadone": 'Ferdig ({0}, {1} filer)', "u_etaprep": '(forberedar opplasting)', "u_hashdone": 'synfaring ferdig', "u_hashing": 'les', "u_hs": 'serveren tenkjer...', "u_started": "filene blir no lasta opp 🚀", "u_dupdefer": "duplikat; vil bli håndtert åt slutt", "u_actx": "klikk her for å forhindre tap av
          yting ved bytte åt andre vindauge/faner", "u_fixed": "OK!  Løyste seg 👍", "u_cuerr": "kunne ikkje laste opp del {0} av {1};\nsikkert greit, fortsetjar\n\nfil: {2}", "u_cuerr2": "server nekta opplastinga (del {0} av {1});\nprøver igjen senere\n\nfil: {2}\n\nerror ", "u_ehstmp": "prøver igjen; se mld nederst", "u_ehsfin": "server nekta forespørselen om å ferdigstille filen; prøver igjen...", "u_ehssrch": "server nekta forespørselen om å utføre søk; prøver igjen...", "u_ehsinit": "server nekta forespørselen om å begynne ei ny opplasting; prøver igjen...", "u_eneths": "eit problem med nettverket gjorde at avtale om opplasting ikkje kunne inngås; prøver igjen...", "u_enethd": "eit problem med nettverket gjorde at filsjekk ikkje kunne utførast; prøver igjen...", "u_cbusy": "ventar på klarering frå server etter eit lite nettverksglipp...", "u_ehsdf": "serveren er full!\n\nprøver igjen regelmessig,\ni tilfelle nokon ryddar litt...", "u_emtleak1": "uff, det er mulig at nettlesaren din har ei minnelekkasje...\nForeslår", "u_emtleak2": ' helst at du byttar åt https, eller ', "u_emtleak3": ' at du ', "u_emtleakc": 'prøver følgande:\n
          • trykk F5 for å laste sida på nytt
          • så skru av  mt  brytaren under  ⚙️ innstillinger
          • og prøv den same opplastinga igjen
          Opplasting vil gå litt treigare, men det får så vere.\nBeklager bryderiet!\n\nPS: feilen skal vere fikset i chrome v107', "u_emtleakf": 'prøver følgende:\n
          • trykk F5 for å laste sida på nytt
          • så skru på 🥔 ("enkelt UI") i opplastaren
          • og prøv den same opplastingen igjen
          \nPS: Firefox fiksar forhåpentligvis feilen ein eller annen gong', "u_s404": "ikkje funne på serveren", "u_expl": "forklar", "u_maxconn": "dei fleste nettlesarar tillet ikkje meir enn 6, men firefox lar deg øke grensen med connections-per-server i about:config", "u_tu": '

          ADVARSEL: turbo er på,  avbrotne opplastingar vil muligens ikkje oppdagast og gjenopptakast; hald musepeikaren over turbo-knappen for meir info

          ', "u_ts": '

          ADVARSEL: turbo er på,  søkeresultat kan vere feil; hold musepeikaren over turbo-knappen for meir info

          ', "u_turbo_c": "turbo er deaktivert i serverkonfigurasjonen", "u_turbo_g": 'turbo blei deaktivert fordi du ikkje har\nhøve åt å sjå mappeinnhold i dette volumet', "u_life_cfg": 'slett opplasting etter min (eller timar)', "u_life_est": 'opplastingen slettast ---', "u_life_max": 'denne mappa tillet ikkje å \noppbevare filer i meir enn {0}', "u_unp_ok": 'opplasting kan angrast i {0}', "u_unp_ng": 'opplasting kan IKKE angrast', "ue_ro": 'du har ikkje høve åt skriving i denne mappa\n\n', "ue_nl": 'du er ikkje logga inn', "ue_la": 'du er logga inn som "{0}"', "ue_sr": 'du er i filsøk-modus\n\nbytt åt opplasting ved å klikke på forstørringsglaset 🔎 (ved siden av den store FILSØK-knappen) og prøv igjen\n\nsorry', "ue_ta": 'prøv å last opp igjen, det burde fungere no', "ue_ab": "den same filen er under opplasting åt ei anna mappe, og den må fullførast der før fila kan lastast opp andre plassar.\n\nDu kan avbryte og gløyme den påbegynte opplastinga ved hjelp av 🧯 oppe åt venstre", "ur_1uo": "OK: Fila blei lastet opp", "ur_auo": "OK: Alle {0} filene blei lastet opp", "ur_1so": "OK: Fila blei funne på serveren", "ur_aso": "OK: Alle {0} filene blei funne på serveren", "ur_1un": "Opplasting feila!", "ur_aun": "Alle {0} opplastingene gjekk feil!", "ur_1sn": "Fila finnast IKKE på serveren", "ur_asn": "Fann INGEN av dei {0} filene på serveren", "ur_um": "Ferdig;\n{0} opplastingar gjekk bra,\n{1} opplastingar gjekk feil", "ur_sm": "Ferdig;\n{0} filer blei funne,\n{1} filer finnast IKKJE på serveren", "rc_opn": "opne", "rc_ply": "spel av", "rc_pla": "spel av som lyd", "rc_txt": "opne i filvisar", "rc_md": "opne i tekstredigerar", "rc_dl": "Last ned", "rc_zip": "Last ned som arkiv", "rc_cpl": "kopier lenke", "rc_del": "slett", "rc_cut": "klipp ut", "rc_cpy": "kopier", "rc_pst": "Lim inn", "rc_rnm": "endre namn", "rc_nfo": "ny mappe", "rc_nfi": "ny fil", "rc_sal": "vel alle", "rc_sin": "inverter val", "rc_shf": "del denne mappa", "rc_shs": "del markering", "lang_set": "passar det å laste sida på nytt?", "splash": { "a1": "oppdatér", "b1": "heisann   (du er ikkje logga inn)", "c1": "logg ut", "d1": "tilstand", "d2": "vis tilstanden åt alle trådar", "e1": "last innst.", "e2": "les inn konfigurasjonsfiler på nytt$N(kontoer, volum, volumbrytarar)$Nog kartlegg alle e2ds-volum$N$Nmerk: endringer i globale parametrar$Nkrev ein full restart for å gjelde", "f1": "du kan sjå på:", "g1": "du kan laste opp åt:", "cc1": "brytarar og slikt:", "h1": "skru av k304", "i1": "skru på k304", "j1": "k304 bryt tilkoplinga for kvar HTTP 304. Dette hjelp mot visse mellomtjenarar som kan sette seg fast / plutselig sluttar å laste sider, men det sett óg ytinga ned betydelig", "k1": "nullstill innstillinger", "l1": "logg inn:", "ls3": "logg inn", "lu4": "brukarnamn", "lp4": "passord", "lo3": "logg ut “{0}” overalt", "lo2": "avslutt økta på alle nettlesarar", "m1": "velkomen attende,", "n1": "404: filen finnast ikkje  ┐( ´ -`)┌", "o1": 'eller kanskje du ikkje har høve? prøv eit passord eller gå heim', "p1": "403: tilgang nektet  ~┻━┻", "q1": 'prøv eit passord eller gå heim', "r1": "gå heim", ".s1": "kartlegg", "t1": "handling", "u2": "tid sidan nokon sist skreiv åt serveren$N( opplastning / namnendring / ... )$N$N17d = 17 dagar$N1h23 = 1 time 23 minutt$N4m56 = 4 minutt 56 sekund", "v1": "kople åt", "v2": "bruk denne serveren som ein lokal harddisk", "w1": "bytt åt https", "x1": "bytt passord", "y1": "dine delinger", "z1": "lås opp område:", "ta1": "du må skrive eit nytt passord først", "ta2": "gjenta for å stadfeste nytt passord:", "ta3": "fant ein skrivefeil; vennligst prøv igjen", "nop": "FEIL: Passord kan ikkje vere tomt", "nou": "FEIL: Brukarnamn og passord må fyllast ut", "aa1": "innkommande:", "ab1": "skru av no304", "ac1": "skru på no304", "ad1": "no304 stoppar all bruk av cache. Hvis ikkje k304 var nok, prøv denne. Vil mangedoble dataforbruk!", "ae1": "utgående:", "af1": "vis nylig opplasta filer", "ag1": "vis kjente IdP-brukarar", } }; ================================================ FILE: copyparty/web/tl/nor.js ================================================ Ls.nor = { "tt": "Norsk", "cols": { "c": "handlingsknapper", "dur": "varighet", "q": "kvalitet / bitrate", "Ac": "lyd-format", "Vc": "video-format", "Fmt": "format / innpakning", "Ahash": "lyd-kontrollsum", "Vhash": "video-kontrollsum", "Res": "oppløsning", "T": "filtype", "aq": "lydkvalitet / bitrate", "vq": "videokvalitet / bitrate", "pixfmt": "fargekoding / detaljenivå", "resw": "horisontal oppløsning", "resh": "vertikal oppløsning", "chs": "lydkanaler", "hz": "lyd-oppløsning", }, "hks": [ [ "ymse", ["ESC", "lukk saker og ting"], "filbehandler", ["G", "listevisning eller ikoner"], ["T", "miniatyrbilder på/av"], ["⇧ A/D", "ikonstørrelse"], ["ctrl-K", "slett valgte"], ["ctrl-X", "klipp ut valgte"], ["ctrl-C", "kopiér til utklippstavle"], ["ctrl-V", "lim inn (flytt/kopiér)"], ["Y", "last ned valgte"], ["F2", "endre navn på valgte"], "filmarkering", ["space", "marker fil"], ["↑/↓", "flytt markør"], ["ctrl ↑/↓", "flytt markør og scroll"], ["⇧ ↑/↓", "velg forr./neste fil"], ["ctrl-A", "velg alle filer / mapper"], ], [ "navigering", ["B", "mappehierarki eller filsti"], ["I/K", "forr./neste mappe"], ["M", "ett nivå opp (eller lukk)"], ["V", "vis mapper eller tekstfiler"], ["A/D", "panelstørrelse"], ], [ "musikkspiller", ["J/L", "forr./neste sang"], ["U/O", "hopp 10sek bak/frem"], ["0..9", "hopp til 0%..90%"], ["P", "pause, eller start / fortsett"], ["S", "marker spillende sang"], ["Y", "last ned sang"], ], [ "bildeviser", ["J/L, ←/→", "forr./neste bilde"], ["Home/End", "første/siste bilde"], ["F", "fullskjermvisning"], ["R", "rotere mot høyre"], ["⇧ R", "rotere mot venstre"], ["S", "marker bilde"], ["Y", "last ned bilde"], ], [ "videospiller", ["U/O", "hopp 10sek bak/frem"], ["P/K/Space", "pause / fortsett"], ["C", "fortsett til neste fil"], ["V", "gjenta avspilling"], ["M", "lyd av/på"], ["[ og ]", "gjentaksintervall"], ], [ "dokumentviser", ["I/K", "forr./neste fil"], ["M", "lukk tekstdokument"], ["E", "rediger tekstdokument"], ["S", "marker fil (for F2/ctrl-x/...)"], ["Y", "last ned tekstfil"], ["⇧ J", "formattér json"], ] ], "m_ok": "OK", "m_ng": "Avbryt", "enable": "Aktiv", "danger": "VARSKU", "clipped": "kopiert til utklippstavlen", "ht_s1": "sekund", "ht_s2": "sekunder", "ht_m1": "minutt", "ht_m2": "minutter", "ht_h1": "time", "ht_h2": "timer", "ht_d1": "dag", "ht_d2": "dager", "ht_and": " og ", "goh": "kontrollpanel", "gop": 'naviger til mappen før denne">forr.', "gou": 'naviger ett nivå opp">opp', "gon": 'naviger til mappen etter denne">neste', "logout": "Logg ut ", "login": "Logg inn", "access": " tilgang", "ot_close": "lukk verktøy", "ot_search": "`søk etter filer ved å angi filnavn, mappenavn, tid, størrelse, eller metadata som sangtittel / artist / osv.$N$N`foo bar` = inneholder både «foo» og «bar»,$N`foo -bar` = inneholder «foo» men ikke «bar»,$N`^yana .opus$` = starter med «yana», filtype «opus»$N`"try unite"` = «try unite» eksakt$N$Ndatoformat er iso-8601, så f.eks.$N`2009-12-31` eller `2020-09-12 23:30:00`", "ot_unpost": "unpost: slett filer som du nylig har lastet opp; «angre-knappen»", "ot_bup": "bup: tradisjonell / primitiv filopplastning,$N$Nfungerer i omtrent samtlige nettlesere", "ot_mkdir": "mkdir: lag en ny mappe", "ot_md": "new-file: lag en ny tekstfil", "ot_msg": "msg: send en beskjed til serverloggen", "ot_mp": "musikkspiller-instillinger", "ot_cfg": "andre innstillinger", "ot_u2i": 'up2k: last opp filer (hvis du har skrivetilgang) eller bytt til søkemodus for å sjekke om filene finnes et-eller-annet sted på serveren$N$Nopplastninger kan gjenopptas etter avbrudd, skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn [🎈]  (den primitive opplasteren "bup")

          mens opplastninger foregår så vises fremdriften her oppe!', "ot_u2w": 'up2k: filopplastning med støtte for å gjenoppta avbrutte opplastninger -- steng ned nettleseren og dra de samme filene inn i nettleseren igjen for å plukke opp igjen der du slapp$N$Nopplastninger skjer stykkevis for potensielt høyere ytelse, og ivaretar datostempling -- men bruker litt mer prosessorkraft enn [🎈]  (den primitive opplasteren "bup")

          mens opplastninger foregår så vises fremdriften her oppe!', "ot_noie": 'Fungerer mye bedre i Chrome / Firefox / Edge', "ab_mkdir": "lag mappe", "ab_mkdoc": "ny tekstfil", "ab_msg": "send melding", "ay_path": "gå videre til mapper", "ay_files": "gå videre til filer", "wt_ren": "gi nye navn til de valgte filene$NSnarvei: F2", "wt_del": "slett de valgte filene$NSnarvei: ctrl-K", "wt_cut": "klipp ut de valgte filene <small>(for å lime inn et annet sted)</small>$NSnarvei: ctrl-X", "wt_cpy": "kopiér de valgte filene til utklippstavlen$N(for å lime inn et annet sted)$NSnarvei: ctrl-C", "wt_pst": "lim inn filer (som tidligere ble klippet ut / kopiert et annet sted)$NSnarvei: ctrl-V", "wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på en fil)", "wt_selinv": "inverter utvalg", "wt_zip1": "last ned denne mappen som et arkiv", "wt_selzip": "last ned de valgte filene som et arkiv", "wt_seldl": "last ned de valgte filene$NSnarvei: Y", "wt_npirc": "kopiér sang-info (irc-formatert)", "wt_nptxt": "kopiér sang-info", "wt_m3ua": "legg til sang i m3u-spilleliste$N(husk å klikke på 📻copy senere)", "wt_m3uc": "kopiér m3u-spillelisten til utklippstavlen", "wt_grid": "bytt mellom ikoner og listevisning$NSnarvei: G", "wt_prev": "forrige sang$NSnarvei: J", "wt_play": "play / pause$NSnarvei: P", "wt_next": "neste sang$NSnarvei: L", "ul_par": "samtidige handl.:", "ut_rand": "finn opp nye tilfeldige filnavn", "ut_u2ts": "gi filen på serveren samme$Ntidsstempel som lokalt hos deg\">📅", "ut_ow": "overskrive eksisterende filer på serveren?$N🛡️: aldri (finner på et nytt filnavn istedenfor)$N🕒: overskriv hvis serverens fil er eldre$N♻️: alltid, gitt at innholdet er forskjellig$N⏭️: hopp over alle eksisterende filer", "ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk", "ut_ask": 'bekreft filutvalg før opplastning starter">💭', "ut_pot": "forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet", "ut_srch": "utfør søk istedenfor å laste opp --$Nleter igjennom alle mappene du har lov til å se", "ut_par": "sett til 0 for å midlertidig stanse opplastning$N$Nhøye verdier (4 eller 8) kan gi bedre ytelse,$Nspesielt på trege internettlinjer$N$Nbør ikke være høyere enn 1 på LAN$Neller hvis serveren sin harddisk er treg", "ul_btn": "slipp filer / mapper
          her (eller klikk meg)", "ul_btnu": "L A S T   O P P", "ul_btns": "F I L S Ø K", "ul_hash": "befar", "ul_send": " send", "ul_done": "total", "ul_idle1": "ingen handlinger i køen", "ut_etah": "snitthastighet for <em>befaring</em> samt gjenstående tid", "ut_etau": "snitthastighet for <em>opplastning</em> samt gjenstående tid", "ut_etat": "<em>total</em> snitthastighet og gjenstående tid", "uct_ok": "fullført uten problemer", "uct_ng": "fullført under tvil (duplikat, ikke funnet, ...)", "uct_done": "fullført (enten <em>ok</em> eller <em>ng</em>)", "uct_bz": "aktive handlinger (befaring / opplastning)", "uct_q": "køen", "utl_name": "filnavn", "utl_ulist": "vis", "utl_ucopy": "kopiér", "utl_links": "lenker", "utl_stat": "status", "utl_prog": "fremdrift", // må være korte: "utl_404": "404", "utl_err": "FEIL!", "utl_oserr": "OS-feil", "utl_found": "funnet", "utl_defer": "senere", "utl_yolo": "YOLO", "utl_done": "ferdig", "ul_flagblk": "filene har blitt lagt i køen
          men det er en annen nettleserfane som holder på med befaring eller opplastning akkurat nå,
          så venter til den er ferdig først", "ul_btnlk": "bryteren har blitt låst til denne tilstanden i serverens konfigurasjon", "udt_up": "Last opp", "udt_srch": "Søk", "udt_drop": "Slipp filene her", "u_nav_m": '
          hva har du?
          Enter = Filer (én eller flere)\nESC = Én mappe (inkludert undermapper)', "u_nav_b": 'FilerÉn mappe', "cl_opts": "brytere", "cl_hfsz": "filstørrelse", "cl_themes": "utseende", "cl_langs": "språk", "cl_ziptype": "nedlastning av mapper", "cl_uopts": "up2k-brytere", "cl_favico": "favicon", "cl_bigdir": "store mapper", "cl_hsort": "#sort", "cl_keytype": "notasjon for musikalsk dur", "cl_hiddenc": "skjulte kolonner", "cl_hidec": "skjul", "cl_reset": "nullstill", "cl_hpick": "klikk på overskriften til kolonnene du ønsker å skjule i tabellen nedenfor", "cl_hcancel": "kolonne-skjuling avbrutt", "cl_rcm": "høyreklikkmeny", "ct_grid": '田 ikoner', "ct_ttips": 'vis hjelpetekst ved å holde musen over ting">ℹ️ tips', "ct_thumb": 'vis miniatyrbilder istedenfor ikoner$NSnarvei: T">🖼️ bilder', "ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk', "ct_dsel": 'marker filer med klikk-og-dra i ikonvisning">dsel', "ct_dl": 'last ned filer (ikke vis i nettleseren)">dl', "ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯', "ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig', "ct_qdel": 'sletteknappen spør bare én gang om bekreftelse">hurtig🗑️', "ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først', "ct_nsort": 'naturlig sortering (forstår tall i filnavn)">nsort', "ct_utc": 'bruk UTC for alle klokkeslett">UTC', "ct_readme": 'vis README.md nedenfor filene">📜 readme', "ct_idxh": 'vis index.html istedenfor fil-liste">htm', "ct_sbars": 'vis rullgardiner / skrollefelt">⟊', "cut_umod": 'i tilfelle en fil du laster opp allerede finnes på serveren, så skal serverens tidsstempel oppdateres slik at det stemmer overens med din lokale fil (krever rettighetene write+delete)">re📅', "cut_turbo": "forenklet befaring ved opplastning; bør sannsynlig ikke skrus på:$N$Nnyttig dersom du var midt i en svær opplastning som måtte restartes av en eller annen grunn, og du vil komme igang igjen så raskt som overhodet mulig.$N$Nnår denne er skrudd på så forenkles befaringen kraftig; istedenfor å utføre en trygg sjekk på om filene finnes på serveren i god stand, så sjekkes kun om filstørrelsen stemmer. Så dersom en korrupt fil skulle befinne seg på serveren allerede, på samme sted med samme størrelse og navn, så blir det ikke oppdaget.$N$Ndet anbefales å kun benytte denne funksjonen for å komme seg raskt igjennom selve opplastningen, for så å skru den av, og til slutt "laste opp" de samme filene én gang til -- slik at integriteten kan verifiseres\">turbo", "cut_datechk": "har ingen effekt dersom turbo er avslått$N$Ngjør turbo bittelitt tryggere ved å sjekke datostemplingen på filene (i tillegg til filstørrelse)$N$Nburde oppdage og gjenoppta de fleste ufullstendige opplastninger, men er ikke en fullverdig erstatning for å deaktivere turbo og gjøre en skikkelig sjekk\">date-chk", "cut_u2sz": "størrelse i megabyte for hvert bruddstykke for opplastning. Store verdier flyr bedre over atlanteren. Små verdier kan være bedre på særdeles ustabile forbindelser", "cut_flag": "samkjører nettleserfaner slik at bare én $N kan holde på med befaring / opplastning $N -- andre faner må også ha denne skrudd på $N -- fungerer kun innenfor samme domene", "cut_az": "last opp filer i alfabetisk rekkefølge, istedenfor minste-fil-først$N$Nalfabetisk kan gjøre det lettere å anslå om alt gikk bra, men er bittelitt tregere på fiber / LAN", "cut_nag": "meldingsvarsel når opplastning er ferdig$N(kun om nettleserfanen ikke er synlig)", "cut_sfx": "lydvarsel når opplastning er ferdig$N(kun om nettleserfanen ikke er synlig)", "cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$Ngjør https 30% raskere, http 4.5x raskere\">mt", "cut_wasm": "bruk wasm istedenfor nettleserens sha512-funksjon; gir bedre ytelse på chrome-baserte nettlesere, men bruker mere CPU, og eldre versjoner av chrome tåler det ikke (spiser opp all RAM og krasjer)\">wasm", "cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)", "cft_fg": "farge", "cft_bg": "bakgrunnsfarge", "cdt_lim": "maks antall filer å vise per mappe", "cdt_ask": "vis knapper for å laste flere filer nederst på siden istedenfor å gradvis laste mer av mappen når man scroller ned", "cdt_hsort": "`antall sorterings-regler (`,sorthref`) som skal inkluderes når media-URL'er genereres. Hvis denne er 0 så vil sorterings-regler i URL'er hverken bli generert eller lest", "cdt_ren": "bruk egendefinert høyreklikkmeny (den vanlige menyen er tilgjengelig med shift + høyreklikk)\">aktiv", "cdt_rdb": "høyreklikk to ganger for å vise den vanlige høyreklikkmenyen\">x2", "tt_entree": "bytt til mappehierarki$NSnarvei: B", "tt_detree": "bytt til tradisjonell sti-visning$NSnarvei: B", "tt_visdir": "bla ned til den åpne mappen", "tt_ftree": "bytt mellom filstruktur og tekstfiler$NSnarvei: V", "tt_pdock": "vis de overordnede mappene i et panel", "tt_dynt": "øk bredden på panelet ettersom treet utvider seg", "tt_wrap": "linjebryting", "tt_hover": "vis hele mappenavnet når musepekeren treffer mappen$N( gjør dessverre at scrollhjulet fusker dersom musepekeren ikke befinner seg i grøfta )", "ml_pmode": "ved enden av mappen", "ml_btns": "knapper", "ml_tcode": "konvertering", "ml_tcode2": "konverter til", "ml_tint": "tint", "ml_eq": "audio equalizer (tonejustering)", "ml_drc": "compressor (volum-utjevning)", "ml_ss": "spol forbi stillhet", "mt_loop": "spill den samme sangen om og om igjen\">🔁", "mt_one": "spill kun én sang\">1️⃣", "mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefølge\">🔀", "mt_aplay": "forsøk å starte avspilling hvis linken du klikket på for å åpne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres så vil heller ikke nettside-URL'en bli oppdatert med sang-ID'er når musikk spilles, i tilfelle innstillingene skulle gå tapt og nettsiden lastes på ny\">a▶", "mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles", "mt_prescan": "ved behov, bla til neste mappe$Nslik at nettleseren lar oss$Nfortsette å spille musikk\">bla", "mt_fullpre": "hent ned hele neste sang, ikke bare litt:$N✅ skru på hvis nettet ditt er ustabilt,$N❌ skru av hvis nettet ditt er tregt\">full", "mt_fau": "for telefoner: forhindre at avspilling stopper hvis nettet er for tregt til å laste neste sang i tide. Hvis påskrudd, kan forårsake at sang-info ikke vises korrekt i OS'et\">☕️", "mt_waves": "waveform seekbar:$Nvis volumkurve i avspillingsfeltet\">~s", "mt_npclip": "vis knapper for å kopiere info om sangen du hører på\">/np", "mt_m3u_c": "vis knapper for å kopiere de valgte$Nsangene som innslag i en m3u8 spilleliste\">📻", "mt_octl": "integrering med operativsystemet (fjernkontroll, info-skjerm)\">os-ctl", "mt_oseek": "tillat spoling med fjernkontroll$N$Nmerk: på noen enheter (iPhones) så vil$Ndette erstatte knappen for neste sang\">spoling", "mt_oscv": "vis album-cover på infoskjermen\">bilde", "mt_follow": "bla slik at sangen som spilles alltid er synlig\">🎯", "mt_compact": "tettpakket avspillerpanel\">⟎", "mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">oppfrisk", "mt_mloop": "repeter hele mappen\">🔁 gjenta", "mt_mnext": "hopp til neste mappe og fortsett\">📂 neste", "mt_mstop": "stopp avspilling\">⏸ stopp", "mt_cflac": "konverter flac / wav-filer til {0}\">flac", "mt_caac": "konverter aac / m4a-filer til to {0}\">aac", "mt_coth": "konverter alt annet (men ikke mp3) til {0}\">andre", "mt_c2opus": "det beste valget for alle PCer og Android\">opus", "mt_c2owa": "opus-weba, for iOS 17.5 og nyere\">owa", "mt_c2caf": "opus-caf, for iOS 11 tilogmed 17\">caf", "mt_c2mp3": "bra valg for steinalder-utstyr (slår aldri feil)\">mp3", "mt_c2flac": "gir best lydkvalitet, men eter nettet ditt\">flac", "mt_c2wav": "helt rå lydstrøm (bruker enda mere data enn flac)\">wav", "mt_c2ok": "bra valg!", "mt_c2nd": "ikke det foretrukne valget for din enhet, men funker sikkert greit", "mt_c2ng": "ser virkelig ikke ut som enheten din takler dette formatet... men ok, vi prøver", "mt_xowa": "iOS har fortsatt problemer med avspilling av owa-musikk i bakgrunnen. Bruk caf eller mp3 istedenfor", "mt_tint": "nivå av bakgrunnsfarge på søkestripa (0-100),$Ngjør oppdateringer mindre distraherende", "mt_eq": "`aktiver tonekontroll og forsterker;$N$Nboost `0` = normal volumskala$N$Nwidth `1  ` = normal stereo$Nwidth `0.5` = 50% blanding venstre-høyre$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = instrumental :^)$N$Nreduserer også dødtid imellom sangfiler", "mt_drc": "aktiver volum-utjevning (dynamic range compressor); vil også aktivere tonejustering, så sett alle EQ-feltene bortsett fra 'width' til 0 hvis du ikke vil ha noe EQ$N$Nfilteret vil dempe volumet på alt som er høyere enn TRESH dB; for hver RATIO dB over grensen er det 1dB som treffer høyttalerne, så standardverdiene tresh -24 og ratio 12 skal bety at volumet ikke går høyere enn -22 dB, slik at man trygt kan øke boost-verdien i equalizer'n til rundt 0.8, eller 1.8 kombinert med ATK 0 og RLS 90 (bare mulig i firefox; andre nettlesere tar ikke høyere RLS enn 1)$N$Nwikipedia forklarer dette mye bedre forresten", "mt_ss": "`spoler forbi stillhet i sanger; spiller `ffwd` ganger raskere nær start/slutt av sangen når volum er under `vol` og posisjonen er innenfor de første `start`% eller siste `slutt`% av sangen", "mt_ssvt": "volumterskel (0-255)\">volum", "mt_ssts": "aktiv innenfor første % av sangen\">start", "mt_sste": "aktiv innenfor siste % av sangen\">slutt", "mt_sssm": "avspillingshastighetsmultiplikator\">ffwd", "mb_play": "lytt", "mm_hashplay": "spill denne sangen?", "mm_m3u": "trykk Enter/OK for å spille\ntrykk ESC/Avbryt for å redigere", "mp_breq": "krever firefox 82+, chrome 73+, eller iOS 15+", "mm_bload": "laster inn...", "mm_bconv": "konverterer til {0}, vent litt...", "mm_opusen": "nettleseren din forstår ikke aac / m4a;\nkonvertering til opus er nå aktivert", "mm_playerr": "avspilling feilet: ", "mm_eabrt": "Avspillingsforespørselen ble avbrutt", "mm_enet": "Nettet ditt er ustabilt", "mm_edec": "Noe er galt med musikkfilen", "mm_esupp": "Nettleseren din forstår ikke filtypen", "mm_eunk": "Ukjent feil", "mm_e404": "Avspilling feilet: Fil ikke funnet.", "mm_e403": "Avspilling feilet: Tilgang nektet.\n\nKanskje du ble logget ut?\nPrøv å trykk F5 for å laste siden på nytt.", "mm_e415": "Avspilling feilet: Kunne ikke konvertere filen, sjekk serverloggen.", "mm_e500": "Avspilling feilet: Rusk i maskineriet, sjekk serverloggen.", "mm_e5xx": "Avspilling feilet: ", "mm_nof": "finner ikke flere sanger i nærheten", "mm_prescan": "Leter etter neste sang...", "mm_scank": "Fant neste sang:", "mm_uncache": "alle sanger vil lastes på nytt ved neste avspilling", "mm_hnf": "sangen finnes ikke lenger", "im_hnf": "bildet finnes ikke lenger", "f_empty": 'denne mappen er tom', "f_chide": 'dette vil skjule kolonnen «{0}»\n\nfanen for "andre innstillinger" lar deg vise kolonnen igjen', "f_bigtxt": "denne filen er hele {0} MiB -- vis som tekst?", "f_bigtxt2": "vil du se bunnen av filen istedenfor? du vil da også se nye linjer som blir lagt til på slutten av filen i sanntid", "fbd_more": '
          viser {0} av {1} filer; vis {2} eller vis alle
          ', "fbd_all": '
          viser {0} av {1} filer; vis alle
          ', "f_anota": "kun {0} av totalt {1} elementer ble markert;\nfor å velge alt må du bla til bunnen av mappen først", "f_dls": 'linkene i denne mappen er nå\nomgjort til nedlastningsknapper', "f_dl_nd": 'hopper over mappe (bruk zip/tar-nedlasting i stedet):\n', "f_partial": "For å laste ned en fil som enda ikke er ferdig opplastet, klikk på filen som har samme filnavn som denne, men uten .PARTIAL på slutten. Da vil serveren passe på at nedlastning går bra. Derfor anbefales det sterkt å trykke AVBRYT eller Escape-tasten.\n\nHvis du virkelig ønsker å laste ned denne .PARTIAL-filen på en ukontrollert måte, trykk OK / Enter for å ignorere denne advarselen. Slik vil du høyst sannsynlig motta korrupt data.", "ft_paste": "Lim inn {0} filer$NSnarvei: ctrl-V", "fr_eperm": 'kan ikke endre navn:\ndu har ikke “move”-rettigheten i denne mappen', "fd_eperm": 'kan ikke slette:\ndu har ikke “delete”-rettigheten i denne mappen', "fc_eperm": 'kan ikke klippe ut:\ndu har ikke “move”-rettigheten i denne mappen', "fp_eperm": 'kan ikke lime inn:\ndu har ikke “write”-rettigheten i denne mappen', "fr_emore": "velg minst én fil som skal få nytt navn", "fd_emore": "velg minst én fil som skal slettes", "fc_emore": "velg minst én fil som skal klippes ut", "fcp_emore": "velg minst én fil som skal kopieres til utklippstavlen", "fs_sc": "del mappen du er i nå", "fs_ss": "del de valgte filene", "fs_just1d": "du kan ikke markere flere mapper samtidig,\neller kombinere mapper og filer", "fs_abrt": "❌ avbryt", "fs_rand": "🎲 tilfeldig navn", "fs_go": "✅ opprett deling", "fs_name": "navn", "fs_src": "kilde", "fs_pwd": "passord", "fs_exp": "varighet", "fs_tmin": "min", "fs_thrs": "timer", "fs_tdays": "dager", "fs_never": "for evig", "fs_pname": "frivillig navn (blir noe tilfeldig ellers)", "fs_tsrc": "fil/mappe som skal deles", "fs_ppwd": "frivillig passord", "fs_w8": "oppretter deling...", "fs_ok": "trykk Enter/OK for å kopiere linken (for CTRL-V)\ntrykk ESC/Avbryt for å bare bekrefte", "frt_dec": "kan korrigere visse ødelagte filnavn\">url-decode", "frt_rst": "nullstiller endringer (tilbake til de originale filnavnene)\">↺ reset", "frt_abrt": "avbryt og lukk dette vinduet\">❌ avbryt", "frb_apply": "IVERKSETT", "fr_adv": "automasjon basert på metadata
          og / eller mønster (regulære uttrykk)\">avansert", "fr_case": "versalfølsomme uttrykk\">Aa", "fr_win": "bytt ut bokstavene <>:"\\|?* med$Ntilsvarende som windows ikke får panikk av\">win", "fr_slash": "bytt ut bokstaven / slik at den ikke forårsaker at nye mapper opprettes\">ikke /", "fr_re": "`regex-mønster som kjøres på hvert filnavn. Grupper kan leses ut i format-feltet nedenfor, f.eks. `(1)` og `(2)` osv.", "fr_fmt": "`inspirert av foobar2000:$N`(title)` byttes ut med sangtittel,$N`[(artist) - ](title)` dropper [dette] hvis artist er blank$N`$lpad((tn),2,0)` viser sangnr. med 2 siffer", "fr_pdel": "slett", "fr_pnew": "lagre som", "fr_pname": "gi innstillingene dine et navn", "fr_aborted": "avbrutt", "fr_lold": "gammelt navn", "fr_lnew": "nytt navn", "fr_tags": "metadata for de valgte filene (kun for referanse):", "fr_busy": "endrer navn på {0} filer...\n\n{1}", "fr_efail": "endring av navn feilet:\n", "fr_nchg": "{0} av navnene ble justert pga. win og/eller ikke /\n\nvil du fortsette med de nye navnene som ble valgt?", "fd_ok": "sletting OK", "fd_err": "sletting feilet:\n", "fd_none": "ingenting ble slettet; kanskje avvist av serverkonfigurasjon (xbd)?", "fd_busy": "sletter {0} filer...\n\n{1}", "fd_warn1": "SLETT disse {0} filene?", "fd_warn2": "Siste sjanse! Dette kan ikke angres. Slett?", "fc_ok": "klippet ut {0} filer", "fc_warn": 'klippet ut {0} filer\n\nmen: kun denne nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides', "fcc_ok": "kopierte {0} filer til utklippstavlen", "fcc_warn": 'kopierte {0} filer til utklippstavlen\n\nmen: kun denne nettleserfanen har mulighet til å lime dem inn et annet sted, siden antallet filer er helt hinsides', "fp_apply": "bekreft og lim inn nå", "fp_skip": "kun ledige", "fp_ecut": "du må klippe ut eller kopiere noen filer / mapper først\n\nmerk: du kan gjerne jobbe på kryss av nettleserfaner; klippe ut i én fane, lime inn i en annen", "fp_ename": "{0} filer kan ikke flyttes til målmappen fordi det allerede finnes filer med samme navn. Gi dem nye navn nedenfor, eller gi dem et blankt navn (\"kun ledige\") for å hoppe over dem:", "fcp_ename": "{0} filer kan ikke kopieres til målmappen fordi det allerede finnes filer med samme navn. Gi dem nye navn nedenfor, eller gi dem et blankt navn (\"kun ledige\") for å hoppe over dem:", "fp_emore": "det er fortsatt flere navn som må endres", "fp_ok": "flytting OK", "fcp_ok": "kopiering OK", "fp_busy": "flytter {0} filer...\n\n{1}", "fcp_busy": "kopierer {0} filer...\n\n{1}", "fp_abrt": "avbryter...", "fp_err": "flytting feilet:\n", "fcp_err": "kopiering feilet:\n", "fp_confirm": "flytt disse {0} filene hit?", "fcp_confirm": "kopiér disse {0} filene hit?", "fp_etab": 'kunne ikke lese listen med filer ifra den andre nettleserfanen', "fp_name": "Laster opp én fil fra enheten din. Velg filnavn:", "fp_both_m": '
          hva skal limes inn her?
          Enter = Flytt {0} filer fra «{1}»\nESC = Last opp {2} filer fra enheten din', "fcp_both_m": '
          hva skal limes inn her?
          Enter = Kopiér {0} filer fra «{1}»\nESC = Last opp {2} filer fra enheten din', "fp_both_b": 'FlyttLast opp', "fcp_both_b": 'KopiérLast opp', "mk_noname": "skriv inn et navn i tekstboksen til venstre først :p", "nmd_i1": "legg også til ønsket filtype, for eksempel .md", "nmd_i2": "du kan bare lage .{0}-filer fordi du ikke har delete-tilgang", "tv_load": "Laster inn tekstfil:\n\n{0}\n\n{1}% ({2} av {3} MiB lastet ned)", "tv_xe1": "kunne ikke laste tekstfil:\n\nfeil ", "tv_xe2": "404, Fil ikke funnet", "tv_lst": "tekstfiler i mappen", "tvt_close": "gå tilbake til mappen$NSnarvei: M (eller Esc)\">❌ lukk", "tvt_dl": "last ned denne filen$NSnarvei: Y\">💾 last ned", "tvt_prev": "vis forrige dokument$NSnarvei: i\">⬆ forr.", "tvt_next": "vis neste dokument$NSnarvei: K\">⬇ neste", "tvt_sel": "markér filen   ( for utklipp / sletting / ... )$NSnarvei: S\">merk", "tvt_j": "formattér json$NSnarvei: shift-J\">j", "tvt_edit": "redigér filen$NSnarvei: E\">✏️ endre", "tvt_tail": "overvåk filen for endringer og vis nye linjer i sanntid\">📡 følg", "tvt_wrap": "tekstbryting\">↵", "tvt_atail": "hold de nyeste linjene synlig (lås til bunnen av siden)\">⚓", "tvt_ctail": "forstå og vis terminalfarger (ansi-sekvenser)\">🌈", "tvt_ntail": "maks-grense for antall bokstaver som skal vises i vinduet", "m3u_add1": "sangen ble lagt til i m3u-spillelisten", "m3u_addn": "{0} sanger ble lagt til i m3u-spillelisten", "m3u_clip": "m3u-spillelisten ble kopiert til utklippstavlen\n\nneste steg er å opprette et tekstdokument med filnavn som slutter på .m3u og lime inn spillelisten der", "gt_vau": "ikke vis videofiler, bare spill lyden\">🎧", "gt_msel": "markér filer istedenfor å åpne dem; ctrl-klikk filer for å overstyre$N$N<em>når aktiv: dobbelklikk en fil / mappe for å åpne</em>$N$NSnarvei: S\">markering", "gt_crop": "beskjær ikonene så de passer bedre\">✂", "gt_3x": "høyere oppløsning på ikoner\">3x", "gt_zoom": "zoom", "gt_chop": "trim", "gt_sort": "sorter", "gt_name": "navn", "gt_sz": "størr.", "gt_ts": "dato", "gt_ext": "type", "gt_c1": "reduser maks-lengde på filnavn", "gt_c2": "øk maks-lengde på filnavn", "sm_w8": "søker...", "sm_prev": "søkeresultatene er fra et tidligere søk:\n ", "sl_close": "lukk søkeresultater", "sl_hits": "viser {0} treff", "sl_moar": "hent flere", "s_sz": "størr.", "s_dt": "dato", "s_rd": "sti", "s_fn": "navn", "s_ta": "meta", "s_ua": "up@", "s_ad": "avns.", "s_s1": "større enn ↓ MiB", "s_s2": "mindre enn ↓ MiB", "s_d1": "nyere enn <dato>", "s_d2": "eldre enn", "s_u1": "lastet opp etter", "s_u2": "og/eller før", "s_r1": "mappenavn inneholder", "s_f1": "filnavn inneholder", "s_t1": "sang-info inneholder", "s_a1": "konkrete egenskaper", "md_eshow": "viser forenklet ", "md_off": "[📜readme] er avskrudd i [⚙️] -- dokument skjult", "badreply": "Ugyldig svar ifra serveren", "xhr403": "403: Tilgang nektet\n\nkanskje du ble logget ut? prøv å trykk F5", "xhr0": "ukjent (enten nettverksproblemer eller serverkrasj)", "cf_ok": "beklager -- liten tilfeldig kontroll, alt OK\n\nting skal fortsette om ca. 30 sekunder\n\nhvis ikkeno skjer, trykk F5 for å laste siden på nytt", "tl_xe1": "kunne ikke hente undermapper:\n\nfeil ", "tl_xe2": "404: Mappen finnes ikke", "fl_xe1": "kunne ikke hente filer i mappen:\n\nfeil ", "fl_xe2": "404: Mappen finnes ikke", "fd_xe1": "kan ikke opprette ny mappe:\n\nfeil ", "fd_xe2": "404: Den overordnede mappen finnes ikke", "fsm_xe1": "kunne ikke sende melding:\n\nfeil ", "fsm_xe2": "404: Den overordnede mappen finnes ikke", "fu_xe1": "kunne ikke hente listen med nylig opplastede filer ifra serveren:\n\nfeil ", "fu_xe2": "404: Filen finnes ikke??", "fz_tar": "ukomprimert gnu-tar arkiv, for linux og mac", "fz_pax": "ukomprimert pax-tar arkiv, litt tregere", "fz_targz": "gnu-tar pakket med gzip (nivå 3)$N$NNB: denne er veldig treg;$Nukomprimert tar er bedre", "fz_tarxz": "gnu-tar pakket med xz (nivå 1)$N$NNB: denne er veldig treg;$Nukomprimert tar er bedre", "fz_zip8": "zip med filnavn i utf8 (noe problematisk på windows 7 og eldre)", "fz_zipd": "zip med filnavn i cp437, for høggamle maskiner", "fz_zipc": "cp437 med tidlig crc32,$Nfor MS-DOS PKZIP v2.04g (oktober 1993)$N(øker behandlingstid på server)", "un_m1": "nedenfor kan du angre / slette filer som du nylig har lastet opp, eller avbryte ufullstendige opplastninger", "un_upd": "oppdater", "un_m4": "eller hvis du vil dele nedlastnings-lenkene:", "un_ulist": "vis", "un_ucopy": "kopiér", "un_flt": "valgfritt filter:  filnavn / filsti må inneholde", "un_fclr": "nullstill filter", "un_derr": 'unpost-sletting feilet:\n', "un_f5": 'noe gikk galt, prøv å oppdatere listen eller trykk F5', "un_uf5": "beklager, men du må laste siden på nytt (f.eks. ved å trykke F5 eller CTRL-R) før denne opplastningen kan avbrytes", "un_nou": 'advarsel: kan ikke vise ufullstendige opplastninger akkurat nå; klikk på oppdater-linken om litt', "un_noc": 'advarsel: angring av fullførte opplastninger er deaktivert i serverkonfigurasjonen', "un_max": "viser de første 2000 filene (bruk filteret for å innsnevre)", "un_avail": "{0} nylig opplastede filer kan slettes
          {1} ufullstendige opplastninger kan avbrytes", "un_m2": "sortert etter opplastningstid; nyeste først:", "un_no1": "men nei, her var det jaggu ikkeno som slettes kan", "un_no2": "men nei, her var det jaggu ingenting som passet overens med filteret", "un_next": "slett de neste {0} filene nedenfor", "un_abrt": "avbryt", "un_del": "slett", "un_m3": "henter listen med nylig opplastede filer...", "un_busy": "sletter {0} filer...", "un_clip": "{0} lenker kopiert til utklippstavlen", "u_https1": "du burde", "u_https2": "bytte til https", "u_https3": "for høyere hastighet", "u_ancient": 'nettleseren din er prehistorisk -- mulig du burde bruke bup istedenfor', "u_nowork": "krever firefox 53+, chrome 57+, eller iOS 11+", "tail_2old": "krever firefox 105+, chrome 71+, eller iOS 14.5+", "u_nodrop": 'nettleseren din er for gammel til å laste opp filer ved å dra dem inn i vinduet', "u_notdir": "mottok ikke mappen!\n\nnettleseren din er for gammel,\nprøv å dra mappen inn i vinduet istedenfor", "u_uri": "for å laste opp bilder ifra andre nettleservinduer,\nslipp bildet rett på den store last-opp-knappen", "u_enpot": 'bytt til enkelt UI (gir sannsynlig raskere opplastning)', "u_depot": 'bytt til snæsent UI (gir sannsynlig tregere opplastning)', "u_gotpot": 'byttet til et enklere UI for å laste opp raskere,\n\ndu kan gjerne bytte tilbake altså!', "u_pott": "

          filer:   {0} ferdig,   {1} feilet,   {2} behandles,   {3} i kø

          ", "u_ever": "dette er den primitive opplasteren; up2k krever minst:
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'dette er den primitive opplasteren; up2k er bedre', "u_uput": 'litt raskere (uten sha512)', "u_ewrite": 'du har ikke skrivetilgang i denne mappen', "u_eread": 'du har ikke lesetilgang i denne mappen', "u_enoi": 'filsøk er deaktivert i serverkonfigurasjonen', "u_enoow": "kan ikke overskrive filer her (Delete-rettigheten er nødvendig)", "u_badf": 'Disse {0} filene (av totalt {1}) kan ikke leses, kanskje pga rettighetsproblemer i filsystemet på datamaskinen din:\n\n', "u_blankf": 'Disse {0} filene (av totalt {1}) er blanke / uten innhold; ønsker du å laste dem opp uansett?\n\n', "u_applef": 'Disse {0} filene (av totalt {1}) er antagelig uønskede;\nTrykk OK/Enter for å HOPPE OVER disse filene,\nTrykk Avbryt/ESC for å LASTE OPP disse filene også:\n\n', "u_just1": '\nFunker kanskje bedre hvis du bare tar én fil om gangen', "u_ff_many": 'Hvis du bruker Linux / MacOS / Android, så kan dette antallet filer
          kanskje krasje Firefox! Hvis det skjer, så prøv igjen (eller bruk Chrome).', "u_up_life": "Filene slettes fra serveren {0}\netter at opplastningen er fullført", "u_asku": 'Laste opp disse {0} filene til {1}', "u_unpt": "Du kan angre / slette opplastningen med 🧯 oppe til venstre", "u_bigtab": 'Vil nå vise {0} filer...\n\nDette kan krasje nettleseren din. Fortsette?', "u_scan": 'Leser mappene...', "u_dirstuck": 'Nettleseren din fikk ikke tilgang til å lese følgende {0} filer/mapper, så de blir hoppet over:', "u_etadone": 'Ferdig ({0}, {1} filer)', "u_etaprep": '(forbereder opplastning)', "u_hashdone": 'befaring ferdig', "u_hashing": 'les', "u_hs": 'serveren tenker...', "u_started": "filene blir nå lastet opp 🚀", "u_dupdefer": "duplikat; vil bli håndtert til slutt", "u_actx": "klikk her for å forhindre tap av
          ytelse ved bytte til andre vinduer/faner", "u_fixed": "OK!  Løste seg 👍", "u_cuerr": "kunne ikke laste opp del {0} av {1};\nsikkert greit, fortsetter\n\nfil: {2}", "u_cuerr2": "server nektet opplastningen (del {0} av {1});\nprøver igjen senere\n\nfil: {2}\n\nerror ", "u_ehstmp": "prøver igjen; se mld nederst", "u_ehsfin": "server nektet forespørselen om å ferdigstille filen; prøver igjen...", "u_ehssrch": "server nektet forespørselen om å utføre søk; prøver igjen...", "u_ehsinit": "server nektet forespørselen om å begynne en ny opplastning; prøver igjen...", "u_eneths": "et problem med nettverket gjorde at avtale om opplastning ikke kunne inngås; prøver igjen...", "u_enethd": "et problem med nettverket gjorde at filsjekk ikke kunne utføres; prøver igjen...", "u_cbusy": "venter på klarering ifra server etter et lite nettverksglipp...", "u_ehsdf": "serveren er full!\n\nprøver igjen regelmessig,\ni tilfelle noen rydder litt...", "u_emtleak1": "uff, det er mulig at nettleseren din har en minnelekkasje...\nForeslår", "u_emtleak2": ' helst at du bytter til https, eller ', "u_emtleak3": ' at du ', "u_emtleakc": 'prøver følgende:\n
          • trykk F5 for å laste siden på nytt
          • så skru av  mt  bryteren under  ⚙️ innstillinger
          • og forsøk den samme opplastningen igjen
          Opplastning vil gå litt tregere, men det får så være.\nBeklager bryderiet !\n\nPS: feilen skal være fikset i chrome v107', "u_emtleakf": 'prøver følgende:\n
          • trykk F5 for å laste siden på nytt
          • så skru på 🥔 ("enkelt UI") i opplasteren
          • og forsøk den samme opplastningen igjen
          \nPS: Firefox fikser forhåpentligvis feilen en eller annen gang', "u_s404": "ikke funnet på serveren", "u_expl": "forklar", "u_maxconn": "de fleste nettlesere tillater ikke mer enn 6, men firefox lar deg øke grensen med connections-per-server i about:config", "u_tu": '

          ADVARSEL: turbo er på,  avbrutte opplastninger vil muligens ikke oppdages og gjenopptas; hold musepekeren over turbo-knappen for mer info

          ', "u_ts": '

          ADVARSEL: turbo er på,  søkeresultater kan være feil; hold musepekeren over turbo-knappen for mer info

          ', "u_turbo_c": "turbo er deaktivert i serverkonfigurasjonen", "u_turbo_g": 'turbo ble deaktivert fordi du ikke har\ntilgang til å se mappeinnhold i dette volumet', "u_life_cfg": 'slett opplastning etter min (eller timer)', "u_life_est": 'opplastningen slettes ---', "u_life_max": 'denne mappen tillater ikke å \noppbevare filer i mer enn {0}', "u_unp_ok": 'opplastning kan angres i {0}', "u_unp_ng": 'opplastning kan IKKE angres', "ue_ro": 'du har ikke skrivetilgang i denne mappen\n\n', "ue_nl": 'du er ikke logget inn', "ue_la": 'du er logget inn som "{0}"', "ue_sr": 'du er i filsøk-modus\n\nbytt til opplastning ved å klikke på forstørrelsesglasset 🔎 (ved siden av den store FILSØK-knappen) og prøv igjen\n\nsorry', "ue_ta": 'prøv å laste opp igjen, det burde funke nå', "ue_ab": "den samme filen er allerede under opplastning til en annen mappe, og den må fullføres der før filen kan lastes opp andre steder.\n\nDu kan avbryte og glemme den påbegynte opplastningen ved hjelp av 🧯 oppe til venstre", "ur_1uo": "OK: Filen ble lastet opp", "ur_auo": "OK: Alle {0} filene ble lastet opp", "ur_1so": "OK: Filen ble funnet på serveren", "ur_aso": "OK: Alle {0} filene ble funnet på serveren", "ur_1un": "Opplastning feilet!", "ur_aun": "Alle {0} opplastningene gikk feil!", "ur_1sn": "Filen finnes IKKE på serveren", "ur_asn": "Fant INGEN av de {0} filene på serveren", "ur_um": "Ferdig;\n{0} opplastninger gikk bra,\n{1} opplastninger gikk feil", "ur_sm": "Ferdig;\n{0} filer ble funnet,\n{1} filer finnes IKKE på serveren", "rc_opn": "åpne", "rc_ply": "spill av", "rc_pla": "spill av som lyd", "rc_txt": "åpne i filviser", "rc_md": "åpne i teksteditor", "rc_dl": "Last ned", "rc_zip": "Last ned som arkiv", "rc_cpl": "kopier lenke", "rc_del": "slett", "rc_cut": "klipp ut", "rc_cpy": "kopier", "rc_pst": "Lim inn", "rc_rnm": "gi nytt navn", "rc_nfo": "ny mappe", "rc_nfi": "ny fil", "rc_sal": "velg alle", "rc_sin": "inverter utvalg", "rc_shf": "del denne mappen", "rc_shs": "del markering", "lang_set": "passer det å laste siden på nytt?", "splash": { "a1": "oppdater", "b1": "halloien   (du er ikke logget inn)", "c1": "logg ut", "d1": "tilstand", "d2": "vis tilstanden til alle tråder", "e1": "last innst.", "e2": "leser inn konfigurasjonsfiler på nytt$N(kontoer, volumer, volumbrytere)$Nog kartlegger alle e2ds-volumer$N$Nmerk: endringer i globale parametere$Nkrever en full restart for å ta gjenge", "f1": "du kan betrakte:", "g1": "du kan laste opp til:", "cc1": "brytere og sånt:", "h1": "skru av k304", "i1": "skru på k304", "j1": "k304 bryter tilkoplingen for hver HTTP 304. Dette hjelper mot visse mellomtjenere som kan sette seg fast / plutselig slutter å laste sider, men det reduserer også ytelsen betydelig", "k1": "nullstill innstillinger", "l1": "logg inn:", "ls3": "logg inn", "lu4": "brukernavn", "lp4": "passord", "lo3": "logg ut “{0}” overalt", "lo2": "avslutter økten på alle nettlesere", "m1": "velkommen tilbake,", "n1": "404: filen finnes ikke  ┐( ´ -`)┌", "o1": 'eller kanskje du ikke har tilgang? prøv et passord eller gå hjem', "p1": "403: tilgang nektet  ~┻━┻", "q1": 'prøv et passord eller gå hjem', "r1": "gå hjem", ".s1": "kartlegg", "t1": "handling", "u2": "tid siden noen sist skrev til serveren$N( opplastning / navneendring / ... )$N$N17d = 17 dager$N1h23 = 1 time 23 minutter$N4m56 = 4 minuter 56 sekunder", "v1": "koble til", "v2": "bruk denne serveren som en lokal harddisk", "w1": "bytt til https", "x1": "bytt passord", "y1": "dine delinger", "z1": "lås opp område:", "ta1": "du må skrive et nytt passord først", "ta2": "gjenta for å bekrefte nytt passord:", "ta3": "fant en skrivefeil; vennligst prøv igjen", "nop": "FEIL: Passord kan ikke være blankt", "nou": "FEIL: Både brukernavn og passord må angis", "aa1": "innkommende:", "ab1": "skru av no304", "ac1": "skru på no304", "ad1": "no304 stopper all bruk av cache. Hvis ikke k304 var nok, prøv denne. Vil mangedoble dataforbruk!", "ae1": "utgående:", "af1": "vis nylig opplastede filer", "ag1": "vis kjente IdP-brukere", } }; ================================================ FILE: copyparty/web/tl/pol.js ================================================ // Wiersze kończące się na //m to niezweryfikowane tłumaczenia maszynowe Ls.pol = { "tt": "Polski", "cols": { "c": "przyciski akcji", "dur": "czas trwania", "q": "jakość / bitrate", "Ac": "kodek audio", "Vc": "kodek wideo", "Fmt": "format / kontener", "Ahash": "suma kontrolna audio", "Vhash": "suma kontrolna wideo", "Res": "rozdzielczość", "T": "rodzaj pliku", "aq": "jakość / bitrate audio", "vq": "jakość / bitrate wideo", "pixfmt": "podpróbkowanie / struktura pikseli", "resw": "rozdzielczość pozioma", "resh": "rozdzielczość pionowa", "chs": "kanały audio", "hz": "częstotliwość próbkowania", }, "hks": [ [ "misc", ["ESC", "zamknij różne rzeczy"], "file-manager", ["G", "przełącz widok lista / siatka"], ["T", "przełącz miniaturki / ikony"], ["⇧ A/D", "wielkość miniaturki"], ["ctrl-K", "usuń zaznaczone"], ["ctrl-X", "wytnij zaznaczone do schowka"], ["ctrl-C", "skopiuj zaznaczone do schowka"], ["ctrl-V", "wklej (przenieś/skopiuj) tutaj"], ["Y", "pobierz zaznaczone"], ["F2", "zmień nazwę zaznaczonych"], "file-list-sel", ["spacja", "przełącz zaznaczanie plików"], ["↑/↓", "przenieś kursor zaznaczenia"], ["ctrl ↑/↓", "przenieś kursor i widok"], ["⇧ ↑/↓", "wybierz poprzedni/następny plik"], ["ctrl-A", "wybierz wszystkie pliki/foldery"], ], [ "navigation", ["B", "przełącz ścieżkę nawigacyjną / panel nawigacyjny"], ["I/K", "poprzedni/następny folder"], ["M", "folder nadrzędny (lub zwiń aktualny)"], ["V", "przełącz foldery / pliki tekstowe w panelu nawigacyjnym"], ["A/D", "rozmiar panelu nawigacyjnego"], ], [ "audio-player", ["J/L", "poprzedni/następny utwór"], ["U/O", "przejdź 10 sek. do tyłu/przodu"], ["0..9", "przeskocz do 0%..90%"], ["P", "odtwórz/pauza (również rozpoczyna)"], ["S", "wybierz odtwarzany utwór"], ["Y", "pobierz utwór"], ], [ "image-viewer", ["J/L, ←/→", "poprzednie/następne zdjęcie"], ["Home/End", "pierwsze/ostatnie zdjęcie"], ["F", "pełny ekran"], ["R", "obróć zgodnie ze wskaz. zegara"], ["⇧ R", "obróć przeciwnie do ruchu wskaz. zegara"], ["S", "wybierz zdjęcie"], ["Y", "pobierz zdjęcie"], ], [ "video-player", ["U/O", "przejdź 10 sek. do tyłu/przodu"], ["P/K/Spacja", "odtwórz/pauza"], ["C", "odtwarzaj następne po zakończeniu"], ["V", "odtwarzaj w pętli"], ["M", "wycisz"], ["[ i ]", "ustaw opóźnienie pętli"], ], [ "textfile-viewer", ["I/K", "poprzedni/następny plik"], ["M", "zamknij plik"], ["E", "edytuj plik"], ["S", "wybierz plik (do wycięcia/skopiowania/zmiany nazwy)"], ["Y", "pobierz plik tekstowy"], //m ["⇧ J", "upiększ json"], //m ] ], "m_ok": "OK", "m_ng": "Anuluj", "enable": "Włącz", "danger": "NIEBEZPIECZEŃSTWO", "clipped": "skopiowano do schowka", "ht_s1": "sekunda", "ht_s2": "sekundy", "ht_s5": "sekund", "ht_m1": "minuta", "ht_m2": "minuty", "ht_m5": "minut", "ht_h1": "godzina", "ht_h2": "godziny", "ht_h5": "godzin", "ht_d1": "dzień", "ht_d2": "dni", "ht_and": " i ", "goh": "panel sterowania", "gop": 'poprzedni plik/folder">poprzedni', "gou": 'nadrzędny folder">w górę', "gon": 'następny folder">następny', "logout": "Wyloguj ", "login": "Zaloguj się", //m "access": " dostęp", "ot_close": "zamknij pod-menu", "ot_search": "`szukaj plików po atrybutach, ścieżce / nazwie, tagach muzyki, bądź dowolnej ich kombinacji$N$N`foo bar` = musi zawierać «foo» oraz «bar»,$N`foo -bar` = musi zawierać «foo», lecz nie «bar»,$N`^yana .opus$` = musi zaczynać się od «yana» i być plikiem «opus»$N`"try unite"` = zawierać dokładnie «try unite»$N$Nformatem daty jest iso-8601, czyli$N`2009-12-31` lub `2020-09-12 23:30:00`", "ot_unpost": "unpost: usuń ostatnio przesłane pliki lub przerwij przesyłanie", "ot_bup": "bup: podstawowe przesyłanie danych, wspiera nawet netscape 4.0", "ot_mkdir": "mkdir: tworzy nowy folder", "ot_md": "new-file: tworzy nowy plik tekstowy", //m "ot_msg": "msg: wysyła wiadomość do loga serwera", "ot_mp": "opcje odtwarzacza multimediów", "ot_cfg": "opcje konfiguracji", "ot_u2i": 'up2k: przesyła pliki (jeżeli masz dostęp do zapisu) lub uruchomia tryb wyszukiwania, aby sprawdzić czy już istnieją na serwerze$N$Nprzesyłanie można wznowić, jest wielowątkowe i znaczniki czasu są zachowywane, lecz zużywa więcej procesora niż [🎈]  (podstawowe przesyłanie)

          podczas przesyłania ta ikona zamienia się w wskaźnik postępu!', "ot_u2w": 'up2k: przesyła pliki z możliwością wznowienia (można zamknąć przeglądarkę i dokończyć przesyłanie plików później)$N$Njest wielowątkowy i zachowuje znaczniki czasu plików, lecz zużywa więcej procesora od [🎈]  (podstawowego przesyłania)

          podczas przesyłania ta ikona zamienia się w wskaźnik postępu!', "ot_noie": 'Użyj przeglądarki Chrome / Firefox / Edge', "ab_mkdir": "stwórz folder", "ab_mkdoc": "nowy plik tekstowy", //m "ab_msg": "wyślij wiad. do logów serwera", "ay_path": "przejdź do folderów", "ay_files": "przejdź do plików", "wt_ren": "zmień nazwę zaznaczonych elementów$NSkrót: F2", "wt_del": "usuń zaznaczone elementy$NSkrót: ctrl-K", "wt_cut": "wytnij zaznaczone elementy <small>(aby wkleić gdzie indziej)</small>$NSkrót: ctrl-X", "wt_cpy": "skopiuj zaznaczone pliki do schowka$N(aby wkleić gdzie indziej)$NSkrót: ctrl-C", "wt_pst": "wklej wcześniej wycięte/skopiowane zaznaczenie$NSkrót: ctrl-V", "wt_selall": "zaznacz wszystko$NHotkey: ctrl-A (when file focused)", "wt_selinv": "odwróć zaznaczenie", "wt_zip1": "pobierz folder jako archiwum", "wt_selzip": "pobierz zaznaczone jako archiwum", "wt_seldl": "pobierz zaznaczenie jako oddzielne pliki$NSkrót: Y", "wt_npirc": "skopiuj informacje o utworze w formacie irc", "wt_nptxt": "skopiuj informacje o utworze jako zwykły tekst", "wt_m3ua": "dodaj to playlisty m3u (kliknij 📻copy kliknij)", "wt_m3uc": "skopiuj playlistę m3u do schowka", "wt_grid": "przełącz widok siatki / listy$NSkrót: G", "wt_prev": "poprzeni utwór$NSkrót: J", "wt_play": "odtwórz / pauza$NSkrót: P", "wt_next": "następny utwór$NSkrót: L", "ul_par": "przesyłane równolegle:", "ut_rand": "losuj nazwy plików", "ut_u2ts": "kopiuj znacznik ostatniej modyfikacji$Nz twojego systemu plików na serwer\">📅", "ut_ow": "nadpisywać istniejące pliki na serwerzę?$N🛡️: nigdy (wygeneruje nową nazwę)$N🕒: nadpisz jeśli pliki na serwerze są starsze niż przesyłane$N♻️: zawsze nadpisuj jeśli zawartość plików się różni$N⏭️: bezwarunkowo pomiń wszystkie istniejące pliki", //m "ut_mt": "hashuj inne pliki podczas przesyłania$N$Nmożna wyłączyć w przypadku wystąpienia wąskiego gardła na CPU lub HDD", "ut_ask": 'pytaj o potwierdzenie rozpoczęcia przesyłania">💭', "ut_pot": "przyspiesz przesyłanie na słabszych urządzeniach,$Nupraszczając interfejs", "ut_srch": "nie przesyłaj plików, jedynie sprawdź czy istnieją$Njuż na serwerze (przeskanuje wszystkie foldery dostępne do odczytu)", "ut_par": "zatrzymuje przesyłanie jeśli wynosi 0$N$Nzwiększ w przypadku jeśli twoja sieć jest wolna / ma duże opóźnienia$N$Nustaw wartość 1 w sieci lokalnej lub w przypadku wolnego dysku serwerowego", "ul_btn": "upuść pliki / foldery
          tutaj (lub kliknij mnie)", "ul_btnu": "P R Z E Ś L I J", "ul_btns": "S Z U K A J", "ul_hash": "hashowanie", "ul_send": "przesyłanie", "ul_done": "gotowe", "ul_idle1": "nic się jeszcze nie przesyła", "ut_etah": "średnia prędkość <em>hashowania</em> i przewidywany czas do końca", "ut_etau": "średnia prędkość <em>przesyłania</em> i przewidywany czas do końca", "ut_etat": "średnia prędkość <em>ogólna</em> i przewidywany czas do końca", "uct_ok": "zakończone pomyślnie", "uct_ng": "zakończono niepowodzeniem (odrzucono, nie znaleziono, itp.)", "uct_done": "zakończono z błędami", "uct_bz": "w trakcie (oblicznie sumy kontrolnej, przesyłanie)", "uct_q": "oczekujące", "utl_name": "nazwa pliku", "utl_ulist": "lista", "utl_ucopy": "kopia", "utl_links": "linki", "utl_stat": "status", "utl_prog": "postęp", // keep short: "utl_404": "404", "utl_err": "BŁĄD", "utl_oserr": "błąd OS", "utl_found": "znaleziono", "utl_defer": "opóźnij", "utl_yolo": "YOLO", "utl_done": "gotowe", "ul_flagblk": "pliki zostały zakolejkowane,
          lecz przesyłanie up2k już trwa (w innej zakładce),
          oczekuję na zakończenie", "ul_btnlk": "przełącznik zablokowany przez konfigurację serwera", "udt_up": "Prześlij", "udt_srch": "Szukaj", "udt_drop": "upuść tutaj", "u_nav_m": '
          co my tu mamy?
          Enter = Pliki (jeden lub wiecej)\nESC = Jeden folder (włącznie z podfolderami)', "u_nav_b": 'PlikiJeden folder', "cl_opts": "przełączniki", "cl_hfsz": "rozmiar pliku", //m "cl_themes": "motyw", "cl_langs": "język", "cl_ziptype": "pobieranie folderów", "cl_uopts": "przełączniki przesyłania (up2k)", "cl_favico": "favicon (ikona w przeglądarce)", "cl_bigdir": "duże foldery", "cl_hsort": "#sortowanie", "cl_keytype": "notacja klucza", // not sure "cl_hiddenc": "ukryte kolumny", "cl_hidec": "ukryj", "cl_reset": "zresetuj", "cl_hpick": "kliknij nagłówki kolumn, aby ukryć je w tabeli niżej", "cl_hcancel": "ukrywanie kolumn przerwane", "cl_rcm": "menu kontekstowe", //m "ct_grid": '田 siatka', "ct_ttips": '◔ ◡ ◔">ℹ️ podpowiedzi', "ct_thumb": 'w widoku siatki, przełącz ikony i miniaturki$NSkrót: T">🖼️ miniaturki', "ct_csel": 'użyj CTRL i SHIFT do wybierania plików w widoku siatki">wybierz', "ct_dsel": 'użyj zaznaczania przez przeciąganie w widoku siatki">przeciągnij', //m "ct_dl": 'wymuś pobieranie (nie wyświetlaj inline) po kliknięciu pliku">dl', //m "ct_ihop": 'przejdź do ostatniego pliku po zamknięciu przeglądarki obrazów">g⮯', "ct_dots": 'pokaż ukryte pliki (jeśli pozwala serwer)">ukryte', "ct_qdel": 'pytaj o potwierdzenie przy usuwaniu tylko raz">pyt. us.', "ct_dir1st": 'pokazuj foldery na początku">📁 najpierw', "ct_nsort": 'naturalne sortowanie (dla numerowanych plików)">nsort', "ct_utc": 'pokaż wszystkie daty/czas w UTC">UTC', "ct_readme": 'pokazuj README.md w folderach">📜 readme', "ct_idxh": 'pokazuj plik index.html zamiast zawartości folderu">htm', "ct_sbars": 'pokazuj paski przewijania">⟊', "cut_umod": "uaktualnij znacznik ostatniej modyfikacji pliku, tak aby pasował do pliku lokalnego jeżeli plik już istnieje na serwerze (wymaga dostępu zapisu i usuwania)\">📅 ponownie", "cut_turbo": "przycisk „raz się żyje”, raczej NIE POWINIENEŚ tego włączać:$N$Nużywaj jeśli przesyłano ogromną liczbę plików i z jakiegoś powodu musisz przesłać pliki ponownie, kontynuując jak najszybciej$N$Nzamienia sprawdzanie sumy kontrolnej plików prostym "czy ten plik jest tego samego rozmiaru jak ten na serwerze?" więc jeśli pliki różnią się zawartością, ale są tego samego rozmiaru, NIE ZOSTANĄ przesłane ponownie$N$Nta opcja powinna zostać wyłączona po zakończeniu przesyłania, i potem "przesłać" te same pliki ponownie w celu weryfikacji\">turbo", "cut_datechk": "przy wyłączonym przycisku turbo nic nie robi$N$Nleciutko zmniejsza czynnik „raz się żyje”; dodatkowo sprawdza czy znaczniki modyfikacji pliku przesyłanego zgadzają się z serwerem$N$Nteorytycznie powinno złapać to większość niedokończonych / uszkodzonych plików, lecz nie jest zamiennikiem wykonania ponownego sprawdzenia bez włączonego trybu turbo\">spr-daty", "cut_u2sz": "rozmiar (w MiB) każdego kawałka do przesłania; większe wartości szybciej latają po Atlantyku. Mniejsze wartości działają lepiej na bardzo niestabilnych połączeniach (neostrada?)", "cut_flag": "zapewnia, że tylko jedna karta przesyła dane w danym momencie$N -- opcja musi być włączona na innych kartach $N - dotyczy tylko kart w tej samej domenie", "cut_az": "przesyła pliki w kolejności alfabetycznej, zamiast rozpocząć od najmniejszego pliku$N$Nkolejność alfabetyczna może ułatwić oszacowanie, co mogło pójść nie tak na serwerze, lecz lekko spowalnia przesyłanie po światłowodzie lub w sieci lokalnej", "cut_nag": "powiadomienie systemowe po zakończeniu przesyłania$N(tylko jeśli przeglądarka lub karta nie jest aktywna)", "cut_sfx": "sygnał dźwiękowy po zakończeniu przesyłania$N(tylko jeśli przeklądarka lub karta nie jest aktywna)", "cut_mt": "używaj wielowątkowości, aby przyspieszyć obliczanie sumy kontrolnej plików$N$Nużywa web workerów i wymaga$Nwięcej pamięci RAM (do 512 MiB)$N$Nprzyspiesza https o 30% i http 4,5-krotnie\">ww", "cut_wasm": "używaj WASM zamiast wbudowanego hashera przeglądarki; zwiększa prędkość na Chrome'o-pochodnych przeglądarkach, zwiększając zużycie procesora, ponadto wiele starszych wersji Chrome'a zawiera błędy powodujące zeżarcie całej pamięci RAM komputera i przymusowe zamknięcie przeglądarki jeżeli ta opcja jest włączona\">wasm", "cft_text": "tekst favicon (aby wyłączyć, usuń zawartość i przeładuj stronę)", "cft_fg": "kolor tekstu", "cft_bg": "kolor tła", "cdt_lim": "maksymalna liczba plików do pokazania na raz w folderze", "cdt_ask": "przy przewijaniu w dół,$Nzapytaj co robić,$Nzamiast wczytywać kolejne pliki", "cdt_hsort": "`ile zasad sortowania (`,sorthref`) zawierać w generowanych linkach multimediów. Wartość 0 sprawi, że zasady sortowania zawarte w linkach multimediów przy otwarciu również będą ignorowane", "cdt_ren": "włącz niestandardowe menu kontekstowe, standardowe menu jest dostępne po wciśnięciu shift i kliknięciu prawym przyciskiem\">włącz", //m "cdt_rdb": "pokaż standardowe menu prawego przycisku, gdy niestandardowe jest już otwarte i nastąpi ponowne kliknięcie\">x2", //m "tt_entree": "pokaż panel nawigacyjny (panel boczny z drzewem folderów)$NSkrót: B", "tt_detree": "pokaż ślad nawigacyjny$NSkrót: B", "tt_visdir": "przewiń do wybranego folderu", "tt_ftree": "przełącz drzewo folderów / pliki tekstowe$NSkrót: V", "tt_pdock": "pokaż foldery nadrzędne w przypiętym u góry panelu", "tt_dynt": "rozszerzaj panel wraz z drzewem", "tt_wrap": "zawijaj tekst", "tt_hover": "pokazuj za długie linie po najechaniu kursorem$N( psuje przewijanie gdy $N  kursor nie jest w lewym marginesie )", "ml_pmode": "na końcu folderu...", "ml_btns": "komendy", "ml_tcode": "transkoduj", "ml_tcode2": "transkoduj do", "ml_tint": "odcień", "ml_eq": "korektor dźwięku (equalizer)", "ml_drc": "kompresor zasięgu dynamiki", "ml_ss": "pomijaj ciszę", //m "mt_loop": "pętla/powtarzaj jeden utwór\">🔁", "mt_one": "zatrzymaj po jednym utworze\">1️⃣", "mt_shuf": "odtwarzaj losowo w każdym folderze\">🔀", "mt_aplay": "autoodtwarzanie po kliknięciu linku do tego serwera, zawierającego identyfikator utworu$N$Nwyłączenie tej opcji zapobiegnie aktualizowaniu adresu strony podczas odtwarzania muzyki, aby zapobiec autoodtwarzaniu przy utracie ustawień\">a▶", "mt_preload": "rozpocznij ładowanie kolejnego utworu blisko końca aktualnego w celu uzyskania odtwarzania bez przerw\">preload", "mt_prescan": "przechodzi do następnego folderu przed zakończeniem ostatniego utworu,$Naby udobruchać przeglądarkę,$Nżeby nie zatrzymała odtwarzania\">naw", "mt_fullpre": "próbuj zbuforować cały utwór;$N✅ włącz na niestabilnych połączeniach,$N❌ wyłącz na wolnych połączeniach\">pełnebuf", "mt_fau": "nie zatrzymuj muzyki jeśli następna piosenka będzie się zbyt wolno buforować na telefonach (może sprawić, że tagi będą się niepoprawnie wyświetlać)\">☕️", "mt_waves": "falisty pasek:$Npokazuj amplitudę dźwięku w pasku utworu\">~s", "mt_npclip": "pokaż przyciski kopiowania aktualnie odtwarzanego utworu\">/np", "mt_m3u_c": "pokaż przyciski kopiowania$Nwybranych piosenek jako playlista m3u8\">📻", "mt_octl": "integracja z systemem operacyjnym (przyciski multimedialne / informacje o utworze)\">os-int", "mt_oseek": "zezwól na przewijanie utworu poprzez integrację z systemem$N$Nuwaga: na niektórych urządzeniach (iPhone'y),$Nzamienia przycisk następnej piosenki\">seek", "mt_oscv": "pokaż okładkę albumu w widoku systemu\">okładka", "mt_follow": "podążaj za odtwarzanym utworem przewijając widok\">🎯", "mt_compact": "kompaktowe sterowanie\">⟎", "mt_uncache": "wyczyść pamięć podręczną  (spróbuj jeśli przeglądarka$Nzachowała zepsutą kopię utworu, przez co nie odtwarza się ona)\">uncache", "mt_mloop": "odtwarzaj utwory w folderze w pętli\">🔁 loop", "mt_mnext": "wczytaj następny folder i kontynuuj\">📂 next", "mt_mstop": "zatrzymaj odtwarzanie\">⏸ stop", "mt_cflac": "przekonwertuj format flac / wav na {0}\">flac", "mt_caac": "przekonwertuj format aac / m4a na {0}\">aac", "mt_coth": "przekonwertuj wszystkie inne formaty (nie będące mp3) na {0}\">oth", "mt_c2opus": "najlepszy wybór dla komputerów, laptopów i urządzeń z androidem\">opus", "mt_c2owa": "opus-weba, dla iOS 17.5 i nowszych\">owa", "mt_c2caf": "opus-caf, dla iOS 11 do 17\">caf", "mt_c2mp3": "używaj na bardzo starych urządzeniach\">mp3", "mt_c2flac": "najlepsza jakość dźwięku, ale ogromne pliki do pobrania\">flac", //m "mt_c2wav": "nieskompresowane odtwarzanie (jeszcze większe)\">wav", //m "mt_c2ok": "cudownie, dobry wybór", "mt_c2nd": "ten format nie jest rekomendowany dla twojego urządzenia, ale nadal jest w porządku", "mt_c2ng": "wygląda na to, że to urządzenie nie wspiera tego formatu, lecz spróbujmy i tak", "mt_xowa": "iOS zawiera błędy uniemożliwiające odtwarzanie w tle używając tego formatu; wybierz caf lub mp3", "mt_tint": "jasność tła (0-100) paska,$Naby zmniejszyć widoczność buforowania", "mt_eq": "`włącza korektor dźwięku (equalizer) i kontrolę wzmocnienia dźwięku;$N$Nboost `0` = standardowa głośność 100% (niezmodyfikowana)$N$Nwidth `1  ` = standardowe stereo (niezmodyfikowane)$Nwidth `0.5` = 50% crossfeed lewo-prawo$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = usuwanie wokalu :^)$N$Nwłączenie korektora sprawia, że albumy bezprzerwowe są w pełni bez przerw, więc jeśli jest to dla ciebie ważne, zostaw wszystko na 0 (poza width = 1)", "mt_drc": "włącza kompresor zakresu dynamiki (normalizacja głośności); włącza również korektor w celu zbalansowania tego spaghetti, więc ustaw wszystkie opcje korektora, oprócz 'width',na 0, jeśli go nie chcesz$N$Nobniża głośność audio nad THRESHOLD (próg) dB; dla każdego RATIO (współczynnika) dB, będącego ponad THRESHOLDem jest 1 dB wyjścia, więc domyślne wartości progu -24 i współczynnika 12 znaczą, że nigdy nie powinno być głośniej niż -22 dB i bezpieczne jest zwiększenie wzmocnienia korektora do 0.8, lub nawet 1.8 z ATK 0 i ogromnym RLS, jak 90 (działa tylko na firefoxie, inne przeglądarki mają limit RLS 1)$N$N(na wikipedii tłumaczą to dużo lepiej)", "mt_ss": "`włącza pomijanie ciszy; mnoży prędkość odtwarzania przez `szyb` blisko początku/końca, gdy głośność jest poniżej `gł` i pozycja w pierwszych `pocz`% lub ostatnich `kon`%", //m "mt_ssvt": "próg głośności (0-255)\">gł", //m "mt_ssts": "aktywny próg (% utworu, początek)\">pocz", //m "mt_sste": "aktywny próg (% utworu, koniec)\">kon", //m "mt_sssm": "mnożnik prędkości odtwarzania\">szyb", //m "mb_play": "odtwórz", "mm_hashplay": "odtworzyć ten plik audio?", "mm_m3u": "naciśnij Enter/OK, aby odtworzyć\nnaciśnij ESC/Anuluj, aby edytować", "mp_breq": "wymagany jest Firefox 82+, Chrome 73+ lub iOS 15+", "mm_bload": "wczytywanie...", "mm_bconv": "konwertowanie do {0}, proszę czekać...", "mm_opusen": "ta przeglądarka nie może odtwarzać plików aac / m4a;\ntranskodowanie do formatu opus włączone", "mm_playerr": "odtwarzanie nie powiodło się: ", "mm_eabrt": "Odtwarzanie zostało przerwane", "mm_enet": "Połączenie z internetem jest słabe", "mm_edec": "Ten plik wydaje się uszkodzony??", "mm_esupp": "Twoja przeglądarka nie rozumie tego formatu audio", "mm_eunk": "Nieznany błąd", "mm_e404": "Nie można odtworzyć; błąd 404: Nie znaleziono pliku.", "mm_e403": "Nie można odtworzyć; błąd 403: Odmowa dostępu.\n\nSpróbuj przeładować stronę (F5), może cię wylogowało", "mm_e415": "Nie można odtworzyć; błąd 415: Konwersja pliku nie powiodła się; sprawdź logi serwera.", //m "mm_e500": "Nie można odtworzyć; błąd 500: Sprawdź logi serwera.", "mm_e5xx": "Nie można odtworzyć; błąd serwera", "mm_nof": "nie znaleziono więcej plików audio", "mm_prescan": "Szukanie kolejnego utworu...", "mm_scank": "Znaleziono następną piosenkę:", "mm_uncache": "wyczyszczono pamięć podręczną; wszystkie utwory zostaną pobrane ponownie przy następnym odtworzeniu", "mm_hnf": "ten utwór już nie istnieje", "im_hnf": "ten obraz już nie istnieje", "f_empty": 'ten folder jest pusty', "f_chide": 'schowa kolumnę «{0}»\n\nkolumny można ponownie pokazać w zakładce ustwaień', "f_bigtxt": "ten plik waży {0} MiB -- na pewno pokazać jako tekst?", "f_bigtxt2": "odczytać jedynie koniec pliku? włączy również śledzenie, pokazując nowo-dodane linie tekstu w czasie rzeczywistym", "fbd_more": '
          pokazuję {0} z {1} plików; pokaż {2} lub pokaż wszystko
          ', "fbd_all": '
          pokazuję {0} z {1} files; pokaż wszystko
          ', "f_anota": "{0} z {1} elementów zostało wybranych;\naby pokazać cały folder, zjedź na dół", "f_dls": 'linki do plików w aktualnym folderze\nzostały zmienione w linki pobierania', "f_dl_nd": 'pomijanie folderu (użyj zamiast tego pobierania zip/tar):\n', //m "f_partial": "Aby bezpiecznie pobrać plik, który aktualnie jest przesyłany, wybierz plik o tej samej nazwie, lecz bez rozszerzenia .PARTIAL. Żeby to zrobić, naciśnij ANULUJ lub klawisz ESC.\n\nWciśnięcie OK / Enter zignoruje to ostrzeżenie i pobierze plik tymczasowy .PARTIAL, który prawie z pewnością będzie zepsuty", "ft_paste": "wklej {0} elementów$NSkrót: ctrl-V", "fr_eperm": 'nie można zmienić nazwy:\nnie posiadasz uprawnienia „move” w tym folderze', "fd_eperm": 'nie można usunąć:\nnie posiadasz uprawnienia „delete” w tym folderze', "fc_eperm": 'nie można wyciąć:\nnie posiadasz uprawnienia „move” w tym folderze', "fp_eperm": 'nie można wkleić:\nnie posiadasz uprawnienia „write” w tym folderze', "fr_emore": "wybierz przynajmniej jeden element do zmiany nazwy", "fd_emore": "wybierz przynajmniej jeden element do usunięcia", "fc_emore": "wybierz przynajmniej jeden element do wycięcia", "fcp_emore": "wybierz przynajmniej jeden element do skopiowania", "fs_sc": "udostępnij ten folder", "fs_ss": "udostępnij zaznaczone pliki", "fs_just1d": "nie można wybrać więcej niż jednego folderu,\nani mieszać plików i folderów w jednym zaznaczeniu", "fs_abrt": "❌ przerwij", "fs_rand": "🎲 losuj nazwę", "fs_go": "✅ stwórz udostępnienie", "fs_name": "nazwa", "fs_src": "źródło", "fs_pwd": "hasło", "fs_exp": "wygaśnięcie", "fs_tmin": "min", "fs_thrs": "godz.", "fs_tdays": "dni", "fs_never": "na zawsze", "fs_pname": "opcjonalna nazwa linku; zostanie wylosowana jeśli pusta", "fs_tsrc": "plik lub folder do udostępnienia", "fs_ppwd": "hasło (opcjonalnie)", "fs_w8": "udostępnianie...", "fs_ok": "naciśnij Enter/OK, aby skopiować do schowka\nnaciśnij ESC/Anuluj, aby zamknąć", "frt_dec": "może naprawić niektóre zepsute nazwy plików\">dekoduj-url", "frt_rst": "zresetuj zmodyfikowane nazwy plików do oryginalnych\">↺ zresetuj", "frt_abrt": "przerwij i zamknij to okno\">❌ anuluj", "frb_apply": "ZASTOSUJ ZMIANĘ NAZWY", "fr_adv": "zmiana nazwy hurtowa / metadanych / wzorcem\">zaawansowane", "fr_case": "rozróżnianie wielkości liter w regex\">wlit", "fr_win": "nazwy bezpieczne dla systemu Windows; zamienia symbole <>:"\\|?* na japońskie odpowiedniki\">win", "fr_slash": "zamienia / symbolem, który nie tworzy nowych folderów\">brak /", "fr_re": "`wzorzec wyszukiwania regex stosowany do oryginalnych nazw plików; do grup przechwytywania można się odwołać w polu formatu poniżej, np. `(1)` i `(2)` itd.", "fr_fmt": "`inspirowane programem foobar2000:$N`(title)` zostaje zamienione na tytuł utworu,$N`[(artist) - ](title)` pomija [tą] część jeśli pole artysty jest puste$N`$lpad((tn),2,0)` wyrównuje numer utworu do 2 cyfr (np. 01, 06, 09, 16)", "fr_pdel": "usuń", "fr_pnew": "zapisz jako", "fr_pname": "podaj nazwę nowego szablonu", "fr_aborted": "anulowano", "fr_lold": "poprzednia nazwa", "fr_lnew": "nowa nazwa", "fr_tags": "znaczniki dla wybranych plików (tylko do odczytu, w celach informacyjnych):", "fr_busy": "zmienianie nazwy {0} plików...\n\n{1}", "fr_efail": "zmiana nazwy zakończona niepowodzeniem:\n", "fr_nchg": "{0} nowych nazw zostało zmienionych przez opcje win i/lub brak /\n\nKontynuować ze zmienionymi nazwami?", "fd_ok": "usunięto", "fd_err": "usuwanie zakończone niepowodzeniem:\n", "fd_none": "nie usunięto nic; usunięcie mogło zostać zablokowane przez konfigurację serwera (xbd)?", "fd_busy": "usuwanie {0} elementów...\n\n{1}", "fd_warn1": "USUNĄĆ {0} elementów?", "fd_warn2": "OSTATNIA SZANSA! Tej operacji nie da się cofnąć. Usunąć?", "fc_ok": "wycięto {0} elementów", "fc_warn": 'wycięto {0} elementów,\n\nlecz można je wkleić tylko w tej karcie\n(ze względu na ogromną ilość wybranych elementów)', "fcc_ok": "skopiowano {0} elementów do schowka", "fcc_warn": 'skopiowano {0} elementów,\n\nlecz można je wkleić tylko w tej karcie\n(ze względu na ogromną ilość wybranych elementów)', "fp_apply": "zastosuj te nazwy", "fp_skip": "pomiń konflikty", //m "fp_ecut": "najpierw wytnij lub skopiuj pliki / foldery, aby je wkleić / przenieść\n\nuwaga: można wycinać / wklejać pomiędzy różnymi kartami przeglądarki", "fp_ename": "Nie udało się przenieść {0} elementów, gdyż ich nazwy już istnieją w tym folderze. Nadaj im nowe nazwy poniżej, bądź zostaw pole nazwy puste (\"pomiń konflikty\"), aby je pominąć:", //m "fcp_ename": "Nie udało się przekopiować {0} elementów, gdyż ich nazwy już istnieją w tym folderze. Nadaj im nowe nazwy poniżej, bądź zostaw pole nazwy puste (\"pomiń konflikty\"), aby je pominąć:", //m "fp_emore": "pozostało jeszcze kilka kolizji nazw plików do poprawy", "fp_ok": "przeniesiono", "fcp_ok": "przekopiowano", "fp_busy": "przenoszenie {0} elementów...\n\n{1}", "fcp_busy": "kopiowanie {0} elementów...\n\n{1}", "fp_abrt": "przerywanie...", //m "fp_err": "nie udało się przenieść:\n", "fcp_err": "nie udało się skopiować:\n", "fp_confirm": "przenieść tutaj {0} elementy(ów)?", "fcp_confirm": "skopiować tutaj {0} elementy(ów)?", "fp_etab": 'nie udało się odczytać schowka z innej karty przeglądarki', "fp_name": "przesyłanie pliku z twojego urządzenia. Nadaj nazwę:", "fp_both_m": '
          wybierz metodę wklejenia
          Enter = Przenieś {0} pliki(ów) z «{1}»\nESC = Prześlij {2} pliki(ów) z twojego urządzenia', "fcp_both_m": '
          wybierz metodę wklejenia
          Enter = Skopiuj {0} pliki(ów) z «{1}»\nESC = Prześlij {2} pliki(ów) z twojego urządzenia', "fp_both_b": 'PrzenieśPrześlij', "fcp_both_b": 'KopiujPrześlij', "mk_noname": "wpisz nazwę do pola po lewej zanim to zrobisz :p", "nmd_i1": "możesz też dodać wybrane rozszerzenie, np. .md", //m "nmd_i2": "możesz tworzyć tylko pliki .{0}, ponieważ nie masz uprawnień do usuwania", //m "tv_load": "Wczytywanie pliku tekstowego:\n\n{0}\n\n{1}% (wczytano {2} z {3} MiB)", "tv_xe1": "nie udało się wczytać pliku:\n\nbłąd ", "tv_xe2": "404, nie znaleziono pliku", "tv_lst": "lista plików tekstowych w", "tvt_close": "powróć do widoku folderów$NSkrót: M (lub Esc)\">❌ zamknij", "tvt_dl": "pobierz ten plik$NHotkey: Y\">💾 pobierz", "tvt_prev": "pokaż poprzedni dokument$NSkrót: i\">⬆ poprzedni", "tvt_next": "pokaż następny dokument$NSkrót: K\">⬇ następny", "tvt_sel": "wybierz plik   ( do wycięcia / skopiowania / usunięcia / itp. )$NSkrót: S\">wyb", "tvt_j": "upiększ json$NSkrót: shift-J\">j", //m "tvt_edit": "otwórz plik w edytorze tekstu$NSkrót: E\">✏️ edytuj", "tvt_tail": "śledź zmiany w pliku; pokazuj nowe linie w czasie rzeczywistym\">📡 śledź", "tvt_wrap": "zawijaj tekst\">↵", "tvt_atail": "utrzymuj widok na dole strony\">⚓", "tvt_ctail": "dekoduj kolory terminala (sekwencje sterujące ANSI)\">🌈", "tvt_ntail": "limit przewijania (ile bajtów tekstu przechowywać w pamięci)", "m3u_add1": "dodano utwór do playlisty m3u", "m3u_addn": "dodano {0} utwory(ów) do playlisty m3u", "m3u_clip": "skopiowano playlistę m3u do schowka\n\nutwórz", "gt_vau": "nie pokazuj obrazu, odtwarzaj tylko dźwięk\">🎧", "gt_msel": "wybierz pliki; kliknij plik z wciśniętym klawiszem CTRL, aby zastąpić$N$N<em>gdy tryb jest aktywny, kliknij dwukrotnie na plik / folder, żeby go otworzyć</em>$N$NSkrót: S\">wybierz wiele", "gt_crop": "kadruj miniaturki do środka\">kadruj", "gt_3x": "miniaturki w wysokiej rozdzielczości\">3x", "gt_zoom": "przybliż", "gt_chop": "przytnij", "gt_sort": "sortuj według", "gt_name": "nazwa", "gt_sz": "rozmiar", "gt_ts": "data", "gt_ext": "typ", "gt_c1": "przycinaj większą część nazw plików (pokazuj mniej)", "gt_c2": "przycinaj mniejszą część nazw plików (pokazuj więcej)", "sm_w8": "wyszukiwanie...", "sm_prev": "wyniki wyszukiwania poniżej pochodzą z poprzedniego zapytania:\n ", "sl_close": "zamknij wyniki wyszukiwania", "sl_hits": "pokazuję {0} wyniki(ów)", "sl_moar": "pokaż więcej", "s_sz": "rozmiar", "s_dt": "data", "s_rd": "ścieżka", "s_fn": "nazwa", "s_ta": "znaczniki", "s_ua": "data przesłania", "s_ad": "zaawansowane", "s_s1": "min. rozmiar (MiB)", "s_s2": "maks. rozmiar (MiB)", "s_d1": "min. data iso8601", "s_d2": "maks. data iso8601", "s_u1": "przesłane po", "s_u2": "i/lub przed", "s_r1": "ścieżka zawiera   (oddzielone spacją)", "s_f1": "nazwa zawiera   (odwróć za pomocą -nope)", "s_t1": "znaczniki zawierają   (^=start, koniec=$)", "s_a1": "dokładne właściwości metadanych", "md_eshow": "nie można wyświetlić ", "md_off": "[📜readme] wyłączone w [⚙️] -- dokument ukryty", "badreply": "Nie udało się przeanalizować odpowiedzi serwera", "xhr403": "403: Odmowa dostępu.\n\nSpróbuj przeładować stronę (F5), możliwe, że cię wylogowano", "xhr0": "nieznany (być może utracono połączenie z serwerem, lub jest on nieaktywny)", "cf_ok": "przepraszamy, włączyła się ochrona przed DD" + wah + "oS\n\nwszystko powinno wrócić do normy za około 30 sekund\n\njeśli nic się nie zmieni, naciśnij klawisz F5, aby przeładować stronę", "tl_xe1": "nie można wyświetlić podfolderów:\n\nbłąd ", "tl_xe2": "404: Nie znaleziono folderu", "fl_xe1": "nie można wyświetlić plików w folderze:\n\nbłąd ", "fl_xe2": "404: Nie znaleziono folderu", "fd_xe1": "nie można stworzyć podfolderu:\n\nbłąd ", "fd_xe2": "404: Nie znaleziono folderu nadrzędnego", "fsm_xe1": "nie można wysłać wiadomości:\n\nbłąd ", "fsm_xe2": "404: Nie znaleziono folderu nadrzędnego", "fu_xe1": "nie udało się wczytać listy unpost z serwera:\n\nbłąd ", "fu_xe2": "404: Nie znaleziono pliku??", "fz_tar": "nieskompresowane archiwum gnu-tar (linux / mac)", "fz_pax": "nieskompresowane archiwum tar w formacie pax (wolniejsze)", "fz_targz": "gnu-tar z kompresją gzip poziomu 3.,$N$Nzazwyczaj bardzo wolne, używaj nieskompresowanego tar", "fz_tarxz": "gnu-tar z kompresją xz poziomu 3.$N$Nzazwyczaj bardzo wolne, używaj nieksompresowanego tar", "fz_zip8": "zip z nazwami plików UTF-8 (może działać nieprawidłowo na systemie Windows 7 i starszych)", "fz_zipd": "zip z nazwami plików cp437, dobre dla bardzo starego oprogramowania", "fz_zipc": "cp437 z CRC32 obliczonym wcześniej,$Ndla MS-DOS PKZIP v2.04g (październik 1993)$N(przetwarzanie do pobrania trwa dłużej)", "un_m1": "można usunąć ostatnio przesłane pliki (lub przerwać trwające) poniżej", "un_upd": "odśwież", "un_m4": "lub udostępnij pliki widoczne poniżej:", "un_ulist": "pokaż", "un_ucopy": "kopiuj", "un_flt": "filtruj (opcjonalnie):  URL musi zawierać", "un_fclr": "wyczyść kryteria filtrowania", "un_derr": 'nie udało się usunąć unpost:\n', "un_f5": 'coś poszło nie tak, spróbuj odświeżyć lub wciśnij F5', "un_uf5": "przed przerwaniem przesyłania trzeba odświeżyć stronę (za pomocą CTRL-R lub F5)", "un_nou": 'ostrzeżenie: serwer jest aktualnie zbyt obciążony, żeby pokazać niedokończone przesłania; kliknij link "odśwież" za chwilę', "un_noc": 'ostrzeżenie: unpost w pełni przesłanych plików jest wyłączone/zabronione w konfiguracji serwera', "un_max": "pokazuję pierwsze 2000 plików (użyj filtrowania)", "un_avail": "{0} ostatnio przesłanych elementów może zostać usunięte
          {1} niedokończonych można przerwać", "un_m2": "przesortowano po czasie przesłania; najnowsze elementy pierwsze: ", "un_no1": "cholibka! żaden przesłany element nie jest wystarczająco niedawny", "un_no2": "cholibka! żaden przesłany element pasujący do filtra nie jest wystarczająco niedawny", "un_next": "usuń następne {0} pliki(ów) poniżej", "un_abrt": "przerwij", "un_del": "usuń", "un_m3": "wczytywanie ostatnio przesłanych elementów...", "un_busy": "usuwanie {0} plików...", "un_clip": "skopiowano {0} linków do schowka", "u_https1": "powinieneś przejść", "u_https2": "na HTTPS w celu", "u_https3": "uzyskania lepszej wydajności", "u_ancient": 'twoja przeglądarka jest niezwykle zabytkowa -- powinieneś zamiast tego użyć bup', "u_nowork": "wymaga Firefox 53+, Chrome 57+ lub iOS 11+", "tail_2old": "wymaga Firefox 105+, Chrome 71+ lub iOS 14.5+", "u_nodrop": 'ta przeglądarka jest za stara, nie wspiera przesyłania "przeciągnij i upuść"', "u_notdir": "to nie jest folder!\n\nta przeglądarka jest za stara\nspróbuj przeciągnąć i upuścić", "u_uri": "aby przeciągnąć i upuścić obrazy z innych okien przeglądarki,\nupuść je na duży przycisk przesyłania", "u_enpot": 'przełącz na lekki interfejs (może zwiększyć prędkość przesyłania)', "u_depot": 'przełącz na ładny interfejs (może zmniejszyć prędkośc przesyłania)', "u_gotpot": 'przełączanie na lekki interfejs w celu poprawy prędkości przesyłania,\n\nzawsze można przełączyć się na ładny interfejs!', "u_pott": "

          pliki:   {0} ukończonych,   {1} nie powiodło się,   {2} w trakcie,   {3} oczekujących

          ", "u_ever": "podstawowe przesyłanie; up2k wymaga minimalnie przeglądarek:
          Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1", "u_su2k": 'podstawowe przesyłanie; up2k jest lepszy', "u_uput": 'optymalizuj dla prędkości (pomijając spr. sum kontrolnych)', "u_ewrite": 'nie masz dostępu do zapisu (write) w tym folderze', "u_eread": 'nie masz dostępu do odczytu (read) tego folderu', "u_enoi": 'wyszukiwanie plików jest wyłączone w konfiguracji serwera', "u_enoow": "nadpisanie nie zadziała, wymagany dostęp do usuwania (delete)", "u_badf": '{0} (z {1}) plików zostało pominiętych, prawdopodobnie przez opcje dostępu systemu plików:\n\n', "u_blankf": '{0} (z {1}) plików jest pustych; przesłać mimo to?\n\n', "u_applef": '{0} (z {1}) plików może być niepożądane;\nNaciśnij OK/Enter, aby pominąć je (wypisane poniżej);\nNaciśnij Anuluj/ESC, by je przesłać mimo to:\n\n', "u_just1": '\nTa funkcja może działać lepiej z wybranym jednym plikiem', "u_ff_many": "na systemach Linux / MacOS / Android, ta ilośc plików może spowodować przymusowe zamknięcie przeglądarki Firefox\nw takim przypadku, spróbuj ponownie (lub użyj Chrome'a).", "u_up_life": "Ten przesyłany plik zostanie usunięty z serwera\n{0} po zakończeniu przesyłania", "u_asku": 'prześlij {0} pliki(ów) do {1}', "u_unpt": "można cofnąć / usunąć ten przesłany plik za pomocą 🧯 w lewym górnym rogu", "u_bigtab": 'zaraz pokażę {0} plików\n\nta operacja może zawiesić twoją przeglądarkę, na pewno kontynuować?', "u_scan": 'Skanowanie plików...', "u_dirstuck": 'iterator katalogów utknął podczas próby dostępu poniższych {0} elementów, pominięto:', "u_etadone": 'Ukończono ({0}, {1} plików)', "u_etaprep": '(przygotowywanie do przesłania)', "u_hashdone": 'obliczono sumę kontrolną', "u_hashing": 'obliczanie sumy kontrolnej', "u_hs": 'nawiązywanie połączenia...', "u_started": "rozpoczęto przesyłanie; zobacz w [🚀]", "u_dupdefer": "duplikat; zostanie przetworzony na końcu", "u_actx": "kliknij ten napis, aby zapobiec spadkowi
          wydajności po zmianie aktywnego okna/karty przeglądarki", "u_fixed": "OK!  Naprawiono 👍", "u_cuerr": "nie udało się przesłać fragmentu {0} z {1};\nprawdopodobnie niegroźne, kontynuowanie\n\nplik: {2}", "u_cuerr2": "serwer odrzucił przesyłanie (kawałek {0} z {1});\nspróbuję ponownie później\n\nplik: {2}\n\nbłąd ", "u_ehstmp": "spróbuję ponownie; więcej informacji w prawym dolnym rogu", "u_ehsfin": "serwer odrzucił prośbę o zakończenie przesyłania; próbuję ponownie...", "u_ehssrch": "serwer odrzucił prośbę o wykonanie wyszukania; próbuję ponownie...", "u_ehsinit": "serwer odrzucił prośbę o rozpoczęcie przesyłania; próbuję ponownie...", "u_eneths": "błąd sieci podczas negocjacji warunków przesyłania; próbuję ponownie...", "u_enethd": "błąd sieci podczas sprawdzania istnienia celu; próbuję ponownie...", "u_cbusy": "oczekiwanie na ponowne zaufanie serwera po błędzie sieci...", "u_ehsdf": "brak miejsca na dysku serwera!\n\npróby będą ponawiane na wypadek\nzwolnienia wystarczająco dużo miejsca aby kontynuować", "u_emtleak1": "wygląda na to, że twoja przeglądarka może mieć wyciek pamięci;\n", "u_emtleak2": ' przejdź na HTTPS (zalecane) lub ', "u_emtleak3": ' ', "u_emtleakc": 'spróbuj:\n
          • wciśnij F5, aby odświeżyć stronę
          • wyłącz przycisk  ww  w menu ⚙️ ustawienia
          • i spróbuj przesłać ponownie
          Prędkość przesyłania będzie niższa, ale cóż zrobisz.\nPrzepraszamy za problemu!\n\nPS: Chrome v107 ma poprawkę tego błędu.', "u_emtleakf": 'spróbuj:\n
          • wciśnij F5, aby odświeżyć stronę
          • włącz tryb 🥔 (lekkiego interfejsu) w interfejsie przesyłania
          • i spróbuj przesłać ponownie
          \nPS: Firefox może kiedyś mieć poprawkę tego błędu', "u_s404": "nie znaleziono na serwerze", "u_expl": "wytłumacz", "u_maxconn": "większość przeglądarek ogranicza to do 6, ale Firefox pozwala zwiększyć tą wartość, ustawiając connections-per-server w about:config", "u_tu": '

          UWAGA: tryb turbo włączony,  klient może nie wykryć i nie kontynuować niedokończonych przesłań; patrz wskazówka przycisku turbo

          ', "u_ts": '

          UWAGA: tryb turbo włączony,  wyniki wyszukiwania mogą być niepoprawne; patrz wskazówka przycisku turbo

          ', "u_turbo_c": "tryb turbo jest wyłączony w konfiguracji serwera", "u_turbo_g": "wyłączanie trybu turbo, nie posiadasz dostępu\ndo listy katalogu w tym wolumenie", "u_life_cfg": 'autousuwanie po min (lub godz.)', "u_life_est": 'przesłany plik zostanie usunięty ---', "u_life_max": 'ten folder wymaga\nmaks. czasu do usunięcia równego {0}', "u_unp_ok": 'unpost jest dozwolony przez {0}', "u_unp_ng": 'unpost NIE jest dozwolony', "ue_ro": 'dostęp tylko-do-odczytu\n\n', "ue_nl": 'nie jesteś zalogowany', "ue_la": 'zalogowano jako "{0}"', "ue_sr": 'jesteś w trybie wyszukiwania\n\nprzełącz się na tryb przesyłania, klikając lupę 🔎 (obok przycisku Szukaj), i spróbuj ponownie', "ue_ta": 'spróbuj przesłać ponownie, wszystko powinno być w porządku', "ue_ab": "ten plik już jest przesyłany do innego folderu, przesyłanie musi się zakończyć, zanim będzie mógł być on przesłany gdzie indziej.\n\nMożna przerwać pierwsze przesyłanie za pomocą 🧯 w lewym górnym rogu", "ur_1uo": "OK: Plik przesłany pomyślnie", "ur_auo": "OK: Wszystkie ({0}) pliki zostały przesłane pomyślnie", "ur_1so": "OK: Znaleziono plik na serwerze", "ur_aso": "OK: Znaleziono wszystkie ({0}) pliki na serwerze", "ur_1un": "Przesyłanie nie powiodło się", "ur_aun": "Wszystkie ({0}) przesłania nie powiodły się", "ur_1sn": "NIE znaleziono pliku na serwerze", "ur_asn": "NIE znaleziono {0} plików na serwerze", "ur_um": "Zakończono;\n{0} przesłań OK,\n{1} przesłań nie powiodło się", "ur_sm": "Zakończono;\nznaleziono {0} pliki(ów),\nnie znaleziono {1} pliki(ów) na serwerze", "rc_opn": "otwórz", //m "rc_ply": "odtwórz", //m "rc_pla": "odtwórz jako dźwięk", //m "rc_txt": "otwórz w przeglądarce plików", //m "rc_md": "otwórz w edytorze tekstu", //m "rc_dl": "pobierz", //m "rc_zip": "pobierz jako archiwum", //m "rc_cpl": "kopiuj link", //m "rc_del": "usuń", //m "rc_cut": "wytnij", //m "rc_cpy": "kopiuj", //m "rc_pst": "wklej", //m "rc_rnm": "zmień nazwę", //m "rc_nfo": "nowy folder", //m "rc_nfi": "nowy plik", //m "rc_sal": "zaznacz wszystko", //m "rc_sin": "odwróć zaznaczenie", //m "rc_shf": "udostępnij ten folder", //m "rc_shs": "udostępnij zaznaczenie", //m "lang_set": "odśwież stronę (F5), aby zastosować zmianę.", "splash": { "a1": "odśwież", "b1": "witaj, nieznajomy   (nie jesteś zalogowany)", "c1": "wyloguj się", "d1": "zrzut stosu", "d2": "pokazuje status wszystkich aktywnych wątków", "e1": "przeładuj konfigurację", "e2": "przeładuj pliki konfiguracyjne (konta/wolumeny/flagi wolumenów),$Ni przeskanuje wszystkie wolumeny e2ds$N$Nnotka: zmiany konfiguracji globalnej$Nwymagają pełnego uruchomienia ponownie serwera, aby zaczęły obowiązywać", "f1": "możesz przeglądać:", "g1": "możesz przesyłać do:", "cc1": "inne:", "h1": "wyłącz k304", "i1": "włącz k304", "j1": "włączenie k304 będzie odłączało klienta przy każdorazowym otrzymaniu kodu HTTP 304, co może zapobiec wieszaniu się wadliwych proxy, ale spowolni ogólne działanie", "k1": "zresetuj ustawienia klienta", "l1": "zaloguj się po więcej:", "ls3": "zaloguj się", //m "lu4": "nazwa użytkownika", //m "lp4": "hasło", //m "lo3": "wyloguj “{0}” wszędzie", //m "lo2": "spowoduje to zakończenie sesji we wszystkich przeglądarkach", //m "m1": "Witaj,", "n1": "404 nie znaleziono  ┐( ´ -`)┌", "o1": 'lub możesz nie mieć dostępu -- spróbuj wprowadzić hasło lub przejdź do strony głównej', "p1": "403 odmowa dostępu  ~┻━┻", "q1": 'użyj hasła lub przejdź do strony głównej', "r1": "idź do strony głównej", ".s1": "przeskanuj ponownie", "t1": "akcje", "u2": "czas od ostatniej interakcji z serwerem$N( przesyłania / zmiany nazwy / ... )$N$N17d = 17 dni$N1h23 = 1 godzina 23 minuty$N4m56 = 4 minuty 56 sekund", "v1": "połącz", "v2": "używaj tego serwera jako dysku lokalnego", "w1": "przejdź na HTTPS", "x1": "zmień hasło", "y1": "edytuj udostępnione", "z1": "odblokuj udostępnienie:", "ta1": "najpierw wprowadź nowe hasło", "ta2": "powtórz hasło dla potwierdzenia:", "ta3": "znaleziono literówkę, spróbuj ponownie", "nop": "BŁĄD: Hasło nie może być puste", //m "nou": "BŁĄD: Nazwa użytkownika i/lub hasło nie może być puste", //m "aa1": "pliki przychodzące:", "ab1": "wyłącz no304", "ac1": "włącz no304", "ad1": "włączenie no304 wyłączy przechowywanie jakiejkolwiek pamięci podręcznej. Zmarnuje to olbrzymią ilość ruchu sieciowego!", "ae1": "trwające pobierania:", "af1": "pokaż ostatnio przesłane pliki", "ag1": "pokaż znanych użytkowników IdP", } }; ================================================ FILE: copyparty/web/tl/por.js ================================================ // Linhas que terminam com //m são traduções automáticas não verificadas Ls.por = { "tt": "Português", "cols": { "c": "botões de ação", "dur": "duração", "q": "qualidade / bitrate", "Ac": "codec de áudio", "Vc": "codec de vídeo", "Fmt": "formato / contêiner", "Ahash": "checksum de áudio", "Vhash": "checksum de vídeo", "Res": "resolução", "T": "tipo de arquivo", "aq": "qualidade / bitrate de áudio", "vq": "qualidade / bitrate de vídeo", "pixfmt": "subamostragem / estrutura de pixel", "resw": "resolução horizontal", "resh": "resolução vertical", "chs": "canais de áudio", "hz": "taxa de amostragem", }, "hks": [ [ "diversos", ["ESC", "fechar várias coisas"], "gerenciador de arquivos", ["G", "alternar entre visualização de lista / grade"], ["T", "alternar entre miniaturas / ícones"], ["⇧ A/D", "tamanho da miniatura"], ["ctrl-K", "excluir selecionados"], ["ctrl-X", "recortar seleção para a área de transferência"], ["ctrl-C", "copiar seleção para a área de transferência"], ["ctrl-V", "colar (mover/copiar) aqui"], ["Y", "baixar selecionado"], ["F2", "renomear selecionado"], "seleção de lista de arquivos", ["espaço", "alternar seleção de arquivo"], ["↑/↓", "mover cursor de seleção"], ["ctrl ↑/↓", "mover cursor e visualização"], ["⇧ ↑/↓", "selecionar arquivo anterior/próximo"], ["ctrl-A", "selecionar todos os arquivos / pastas"], ], [ "navegação", ["B", "alternar entre breadcrumbs / painel de navegação"], ["I/K", "pasta anterior/próxima"], ["M", "pasta pai (ou desexpandir a atual)"], ["V", "alternar entre pastas / arquivos de texto no painel de navegação"], ["A/D", "tamanho do painel de navegação"], ], [ "reprodutor de áudio", ["J/L", "música anterior/próxima"], ["U/O", "pular 10 segundos para trás/frente"], ["0..9", "pular para 0%..90%"], ["P", "reproduzir/pausar (também inicia)"], ["S", "selecionar a música que está tocando"], ["Y", "baixar música"], ], [ "visualizador de imagens", ["J/L, ←/→", "imagem anterior/próxima"], ["Home/End", "primeira/última imagem"], ["F", "tela cheia"], ["R", "girar no sentido horário"], ["⇧ R", "girar no sentido anti-horário"], ["S", "selecionar imagem"], ["Y", "baixar imagem"], ], [ "reprodutor de vídeo", ["U/O", "pular 10 segundos para trás/frente"], ["P/K/Espaço", "reproduzir/pausar"], ["C", "continuar reproduzindo o próximo"], ["V", "loop"], ["M", "mudo"], ["[ e ]", "definir intervalo de loop"], ], [ "visualizador de arquivos de texto", ["I/K", "arquivo anterior/próximo"], ["M", "fechar arquivo de texto"], ["E", "editar arquivo de texto"], ["S", "selecionar arquivo (para recortar/copiar/renomear)"], ["Y", "baixar arquivo de texto"], ["⇧ J", "embelezar json"], ] ], "m_ok": "OK", "m_ng": "Cancelar", "enable": "Ativar", "danger": "PERIGO", "clipped": "copiado para a área de transferência", "ht_s1": "segundo", "ht_s2": "segundos", "ht_m1": "minuto", "ht_m2": "minutos", "ht_h1": "hora", "ht_h2": "horas", "ht_d1": "dia", "ht_d2": "dias", "ht_and": " e ", "goh": "painel de controle", "gop": 'pai anterior">anterior', "gou": 'pasta pai">acima', "gon": 'próxima pasta">próximo', "logout": "Sair ", "login": "Fazer login", "access": " acesso", "ot_close": "fechar submenu", "ot_search": "`procurar arquivos por atributos, caminho / nome, tags de música ou qualquer combinação deles$N$N`foo bar` = deve conter ambos «foo» e «bar»,$N`foo -bar` = deve conter «foo» mas não «bar»,$N`^yana .opus$` = começar com «yana» e ser um arquivo «opus»$N`"try unite"` = conter exatamente «try unite»$N$No formato de data é iso-8601, como$N`2009-12-31` ou `2020-09-12 23:30:00`", "ot_unpost": "despublicar: excluir seus uploads recentes, ou abortar os que não foram concluídos", "ot_bup": "bup: uploader básico, até suporta netscape 4.0", "ot_mkdir": "mkdir: criar um novo diretório", "ot_md": "new-file: criar um novo arquivo de texto", "ot_msg": "msg: enviar uma mensagem para o log do servidor", "ot_mp": "opções do reprodutor de mídia", "ot_cfg": "opções de configuração", "ot_u2i": 'up2k: fazer upload de arquivos (se você tiver acesso de escrita) ou alternar para o modo de busca para ver se eles já existem em algum lugar no servidor$N$Nuploads são reiniciáveis, multithread, e os carimbos de data/hora dos arquivos são preservados, mas usa mais CPU que [🎈]  (o uploader básico)

          durante os uploads, este ícone se torna um indicador de progresso!', "ot_u2w": 'up2k: fazer upload de arquivos com suporte a retomada (feche seu navegador e solte os mesmos arquivos mais tarde)$N$Nmultithread, e os carimbos de data/hora dos arquivos são preservados, mas usa mais CPU que [🎈]  (o uploader básico)

          durante os uploads, este ícone se torna um indicador de progresso!', "ot_noie": 'Por favor, use Chrome / Firefox / Edge', "ab_mkdir": "criar diretório", "ab_mkdoc": "novo arquivo de texto", "ab_msg": "enviar msg para o log do srv", "ay_path": "pular para pastas", "ay_files": "pular para arquivos", "wt_ren": "renomear itens selecionados$NHotkey: F2", "wt_del": "excluir itens selecionados$NHotkey: ctrl-K", "wt_cut": "recortar itens selecionados <small>(depois colar em outro lugar)</small>$NHotkey: ctrl-X", "wt_cpy": "copiar itens selecionados para a área de transferência$N(para colá-los em outro lugar)$NHotkey: ctrl-C", "wt_pst": "colar uma seleção previamente recortada / copiada$NHotkey: ctrl-V", "wt_selall": "selecionar todos os arquivos$NHotkey: ctrl-A (quando o arquivo estiver em foco)", "wt_selinv": "inverter seleção", "wt_zip1": "baixar esta pasta como um arquivo compactado", "wt_selzip": "baixar seleção como um arquivo compactado", "wt_seldl": "baixar seleção como arquivos separados$NHotkey: Y", "wt_npirc": "copiar informações da faixa em formato irc", "wt_nptxt": "copiar informações da faixa em texto simples", "wt_m3ua": "adicionar à playlist m3u (clique em 📻copiar depois)", "wt_m3uc": "copiar playlist m3u para a área de transferência", "wt_grid": "alternar entre visualização de grade / lista$NHotkey: G", "wt_prev": "faixa anterior$NHotkey: J", "wt_play": "reproduzir / pausar$NHotkey: P", "wt_next": "próxima faixa$NHotkey: L", "ul_par": "uploads paralelos:", "ut_rand": "randomizar nomes de arquivos", "ut_u2ts": "copiar o carimbo de data/hora de última modificação$Ndo seu sistema de arquivos para o servidor\">📅", "ut_ow": "substituir arquivos existentes no servidor?$N🛡️: nunca (irá gerar um novo nome de arquivo em vez disso)$N🕒: substituir se o arquivo no servidor for mais antigo que o seu$N♻️: sempre substituir se os arquivos forem diferentes$N⏭️: ignorar incondicionalmente todos os arquivos existentes", "ut_mt": "continuar a fazer o hash de outros arquivos enquanto faz upload$N$Ntalvez desativar se sua CPU ou HDD for um gargalo", "ut_ask": 'pedir confirmação antes do upload começar">💭', "ut_pot": "melhorar a velocidade de upload em dispositivos lentos$Ntornando a UI menos complexa", "ut_srch": "não fazer upload, em vez disso verificar se os arquivos já$N existem no servidor (irá escanear todas as pastas que você pode ler)", "ut_par": "pausar uploads definindo para 0$N$Naumentar se sua conexão for lenta / alta latência$N$Nmanter em 1 em LAN ou se o HDD do servidor for um gargalo", "ul_btn": "soltar arquivos / pastas
          aqui (ou clique em mim)", "ul_btnu": "U P L O A D", "ul_btns": "B U S C A R", "ul_hash": "hash", "ul_send": "enviar", "ul_done": "feito", "ul_idle1": "nenhum upload está na fila ainda", "ut_etah": "velocidade média de <em>hash</em>, e tempo estimado até o fim", "ut_etau": "velocidade média de <em>upload</em> e tempo estimado até o fim", "ut_etat": "velocidade média <em>total</em> e tempo estimado até o fim", "uct_ok": "concluído com sucesso", "uct_ng": "ruim: falhou / rejeitado / não encontrado", "uct_done": "ok e ruim combinados", "uct_bz": "fazendo hash ou upload", "uct_q": "ocioso, pendente", "utl_name": "nome do arquivo", "utl_ulist": "lista", "utl_ucopy": "copiar", "utl_links": "links", "utl_stat": "status", "utl_prog": "progresso", // mantenha curto: "utl_404": "404", "utl_err": "ERRO", "utl_oserr": "Erro-SO", "utl_found": "encontrado", "utl_defer": "adiar", "utl_yolo": "YOLO", "utl_done": "feito", "ul_flagblk": "os arquivos foram adicionados à fila
          no entanto, há um up2k ocupado em outra aba do navegador,
          então esperando que ele termine primeiro", "ul_btnlk": "a configuração do servidor bloqueou este interruptor neste estado", "udt_up": "Upload", "udt_srch": "Buscar", "udt_drop": "solte aqui", "u_nav_m": '
          certo, o que você tem?
          Enter = Arquivos (um ou mais)\nESC = Uma pasta (incluindo subpastas)', "u_nav_b": 'ArquivosUma pasta', "cl_opts": "interruptores", "cl_hfsz": "tamanho do arquivo", "cl_themes": "tema", "cl_langs": "idioma", "cl_ziptype": "download de pasta", "cl_uopts": "interruptores up2k", "cl_favico": "favicon", "cl_bigdir": "grandes dirs", "cl_hsort": "#sort", "cl_keytype": "notação de tecla", "cl_hiddenc": "colunas ocultas", "cl_hidec": "ocultar", "cl_reset": "resetar", "cl_hpick": "toque nos cabeçalhos das colunas para ocultá-los na tabela abaixo", "cl_hcancel": "ocultar coluna abortado", "cl_rcm": "menu de clique direito", "ct_grid": '田 a grade', "ct_ttips": '◔ ◡ ◔">ℹ️ dicas de ferramentas', "ct_thumb": 'na visualização de grade, alternar entre ícones ou miniaturas$NHotkey: T">🖼️ miniaturas', "ct_csel": 'usar CTRL e SHIFT para seleção de arquivo na visualização de grade">sel', "ct_dsel": 'usar seleção por arrasto na visualização de grade">arrastar', "ct_dl": 'forçar download (não exibir na página) ao clicar em um arquivo">dl', "ct_ihop": 'quando o visualizador de imagens for fechado, rolar para o último arquivo visualizado">g⮯', "ct_dots": 'mostrar arquivos ocultos (se o servidor permitir)">dotfiles', "ct_qdel": 'ao excluir arquivos, pedir confirmação apenas uma vez">qdel', "ct_dir1st": 'ordenar pastas antes de arquivos">📁 primeiro', "ct_nsort": 'ordem natural (para nomes de arquivos com dígitos iniciais)">nsort', "ct_utc": 'mostrar todas as datas/horas em UTC">UTC', "ct_readme": 'mostrar README.md nas listas de pastas">📜 readme', "ct_idxh": 'mostrar index.html em vez de lista de pastas">htm', "ct_sbars": 'mostrar barras de rolagem">⟊', "cut_umod": "se um arquivo já existe no servidor, atualizar o carimbo de data/hora de última modificação do servidor para corresponder ao seu arquivo local (requer permissões de escrita+exclusão)\">re📅", "cut_turbo": "o botão yolo, você provavelmente NÃO quer habilitar isso:$N$Nuse isto se você estava fazendo upload de uma enorme quantidade de arquivos e teve que reiniciar por algum motivo, e quer continuar o upload o mais rápido possível$N$Nisto substitui a verificação de hash por uma simples "este arquivo tem o mesmo tamanho no servidor?" então se o conteúdo do arquivo for diferente ele NÃO será enviado$N$Nvocê deve desativar isso quando o upload estiver concluído, e então "enviar" os mesmos arquivos novamente para permitir que o cliente os verifique\">turbo", "cut_datechk": "não tem efeito a menos que o botão turbo esteja ativado$N$Nreduz o fator yolo por uma pequena quantidade; verifica se os carimbos de data/hora dos arquivos no servidor correspondem aos seus$N$Ndeve teoricamente pegar a maioria dos uploads incompletos / corrompidos, mas não é um substituto para fazer uma verificação com o turbo desativado depois\">date-chk", "cut_u2sz": "tamanho (em MiB) de cada bloco de upload; valores grandes voam melhor pelo atlântico. Tente valores baixos em conexões muito não confiáveis", "cut_flag": "garantir que apenas uma aba esteja fazendo upload por vez $N -- outras abas devem ter isso ativado também $N -- só afeta abas no mesmo domínio", "cut_az": "enviar arquivos em ordem alfabética, em vez de o menor primeiro$N$Na ordem alfabética pode tornar mais fácil de verificar se algo deu errado no servidor, mas torna o upload um pouco mais lento em fibra / LAN", "cut_nag": "notificação do SO quando o upload for concluído$N(somente se o navegador ou aba não estiver ativo)", "cut_sfx": "alerta audível quando o upload for concluído$N(somente se o navegador ou aba não estiver ativo)", "cut_mt": "usar multithreading para acelerar o hash de arquivos$N$Nisto usa web-workers e requer$Nmais RAM (até 512 MiB extras)$N$Ntorna https 30% mais rápido, http 4.5x mais rápido\">mt", "cut_wasm": "usar wasm em vez do hasher embutido do navegador; melhora a velocidade em navegadores baseados em chrome mas aumenta a carga da CPU, e muitas versões antigas do chrome têm bugs que fazem o navegador consumir toda a RAM e travar se isso for ativado\">wasm", "cft_text": "texto do favicon (deixe em branco e atualize para desativar)", "cft_fg": "cor do primeiro plano", "cft_bg": "cor do fundo", "cdt_lim": "número máximo de arquivos para mostrar em uma pasta", "cdt_ask": "ao rolar para o final,$Nem vez de carregar mais arquivos,$Nperguntar o que fazer", "cdt_hsort": "`quantas regras de ordenação (`,sorthref`) incluir em URLs de mídia. Definir isso para 0 também ignorará as regras de ordenação incluídas em links de mídia quando você clicar neles", "cdt_ren": "ativar menu de clique direito personalizado, o menu normal permanece acessível com shift + clique direito\">ativar", "cdt_rdb": "mostrar o menu padrão do botão direito quando o menu personalizado já estiver aberto e houver um novo clique\">x2", //m "tt_entree": "mostrar painel de navegação (árvore de diretórios)$NHotkey: B", "tt_detree": "mostrar breadcrumbs$NHotkey: B", "tt_visdir": "rolar para a pasta selecionada", "tt_ftree": "alternar entre árvore de pastas / arquivos de texto$NHotkey: V", "tt_pdock": "mostrar pastas pai em um painel acoplado no topo", "tt_dynt": "crescer automaticamente à medida que a árvore se expande", "tt_wrap": "quebra de linha", "tt_hover": "revelar linhas transbordando ao passar o mouse$N( quebra a rolagem a menos que o cursor do mouse $N  esteja na margem esquerda )", "ml_pmode": "ao final da pasta...", "ml_btns": "comandos", "ml_tcode": "transcodificar", "ml_tcode2": "transcodificar para", "ml_tint": "matiz", "ml_eq": "equalizador de áudio", "ml_drc": "compressor de faixa dinâmica", "ml_ss": "ignorar silêncio", //m "mt_loop": "loop/repetir uma música\">🔁", "mt_one": "parar depois de uma música\">1️⃣", "mt_shuf": "embaralhar as músicas em cada pasta\">🔀", "mt_aplay": "reproduzir automaticamente se houver um ID de música no link que você clicou para acessar o servidor$N$Ndesativar isso também impedirá que a URL da página seja atualizada com IDs de música ao tocar música, para evitar a reprodução automática se essas configurações forem perdidas mas a URL permanecer\">a▶", "mt_preload": "começar a carregar a próxima música perto do final para uma reprodução sem interrupções\">preload", "mt_prescan": "ir para a próxima pasta antes que a última música$Ntermine, mantendo o navegador feliz$Npara que ele não pare a reprodução\">nav", "mt_fullpre": "tentar pré-carregar a música inteira;$N✅ ativar em conexões não confiáveis,$N❌ desativar em conexões lentas provavelmente\">full", "mt_fau": "em telefones, evitar que a música pare se a próxima música não pré-carregar rápido o suficiente (pode fazer as tags aparecerem com falhas)\">☕️", "mt_waves": "barra de busca de forma de onda:$Nmostrar amplitude de áudio no scrubber\">~s", "mt_npclip": "mostrar botões para copiar a música que está tocando para a área de transferência\">/np", "mt_m3u_c": "mostrar botões para copiar as$Nmúsicas selecionadas como entradas de playlist m3u8 para a área de transferência\">📻", "mt_octl": "integração com o SO (atalhos de mídia / osd)\">os-ctl", "mt_oseek": "permitir busca através da integração com o SO$N$Nnota: em alguns dispositivos (iPhones),$Nisto substitui o botão de próxima música\">seek", "mt_oscv": "mostrar capa do álbum no osd\">art", "mt_follow": "manter a faixa que está tocando rolando à vista\">🎯", "mt_compact": "controles compactos\">⟎", "mt_uncache": "limpar cache  (tente isso se seu navegador armazenou em cache$Numa cópia quebrada de uma música e se recusa a tocar)\">uncache", "mt_mloop": "loop na pasta aberta\">🔁 loop", "mt_mnext": "carregar a próxima pasta e continuar\">📂 próximo", "mt_mstop": "parar reprodução\">⏸ parar", "mt_cflac": "converter flac / wav para {0}\">flac", "mt_caac": "converter aac / m4a para {0}\">aac", "mt_coth": "converter todos os outros (não mp3) para {0}\">oth", "mt_c2opus": "melhor escolha para desktops, laptops, android\">opus", "mt_c2owa": "opus-weba, para iOS 17.5 e mais recentes\">owa", "mt_c2caf": "opus-caf, para iOS 11 a 17\">caf", "mt_c2mp3": "use isso em dispositivos muito antigos\">mp3", "mt_c2flac": "melhor qualidade de som, mas downloads enormes\">flac", "mt_c2wav": "reprodução não comprimida (ainda maior)\">wav", "mt_c2ok": "legal, boa escolha", "mt_c2nd": "esse não é o formato de saída recomendado para o seu dispositivo, mas tudo bem", "mt_c2ng": "seu dispositivo não parece suportar este formato de saída, mas vamos tentar mesmo assim", "mt_xowa": "existem bugs no iOS que impedem a reprodução em segundo plano usando este formato; por favor, use caf ou mp3 em vez disso", "mt_tint": "nível de fundo (0-100) na barra de busca$Npara tornar o buffer menos distrativo", "mt_eq": "`ativa o equalizador e o controle de ganho;$N$Nimpulsão `0` = volume padrão de 100% (não modificado)$N$Nlargura `1  ` = estéreo padrão (não modificado)$Nlargura `0.5` = 50% de crossfeed esquerda-direita$Nlargura `0  ` = mono$N$Nimpulsão `-0.8` & largura `10` = remoção de vocal :^)$N$Natvar o equalizador torna os álbuns sem interrupções totalmente sem interrupções, então deixe-o ligado com todos os valores em zero (exceto largura = 1) se você se importa com isso", "mt_drc": "ativa o compressor de faixa dinâmica (nivelador de volume / brickwaller); também ativará o EQ para equilibrar o spaghetti, então defina todos os campos EQ exceto 'width' para 0 se você não quiser$N$Nabaixa o volume do áudio acima do THRESHOLD dB; para cada RATIO dB após o THRESHOLD há 1 dB de saída, então os valores padrão de tresh -24 e ratio 12 significam que nunca deve ficar mais alto que -22 dB e é seguro aumentar o impulso do equalizador para 0.8, ou até 1.8 com ATK 0 e um enorme RLS como 90 (só funciona no firefox; RLS é no máximo 1 em outros navegadores)$N$N(veja a wikipedia, eles explicam muito melhor)", "mt_ss": "`ativa pular silêncio; multiplica a velocidade por `av` perto do início/fim quando o volume está abaixo de `vol` e a posição está nos primeiros `ini`% ou últimos `fim`%", //m "mt_ssvt": "limiar de volume (0-255)\">vol", //m "mt_ssts": "limiar ativo (% faixa, início)\">ini", //m "mt_sste": "limiar ativo (% faixa, fim)\">fim", //m "mt_sssm": "multiplicador de velocidade de reprodução\">av", //m "mb_play": "reproduzir", "mm_hashplay": "reproduzir este arquivo de áudio?", "mm_m3u": "pressione Enter/OK para Reproduzir\npressione ESC/Cancelar para Editar", "mp_breq": "precisa do firefox 82+ ou chrome 73+ ou iOS 15+", "mm_bload": "carregando...", "mm_bconv": "convertendo para {0}, por favor, espere...", "mm_opusen": "seu navegador não pode reproduzir arquivos aac / m4a;\na transcodificação para opus agora está ativada", "mm_playerr": "reprodução falhou: ", "mm_eabrt": "A tentativa de reprodução foi cancelada", "mm_enet": "Sua conexão de internet está instável", "mm_edec": "Este arquivo está supostamente corrompido??", "mm_esupp": "Seu navegador não entende este formato de áudio", "mm_eunk": "Erro Desconhecido", "mm_e404": "Não foi possível reproduzir áudio; erro 404: Arquivo não encontrado.", "mm_e403": "Não foi possível reproduzir áudio; erro 403: Acesso negado.\n\nTente pressionar F5 para recarregar, talvez você tenha saído da conta", "mm_e415": "Não foi possível reproduzir áudio; erro 415: Falha na conversão do arquivo; verifique os logs do servidor.", "mm_e500": "Não foi possível reproduzir áudio; erro 500: Verifique os logs do servidor.", "mm_e5xx": "Não foi possível reproduzir áudio; erro do servidor ", "mm_nof": "não encontrando mais arquivos de áudio por perto", "mm_prescan": "Procurando música para tocar a seguir...", "mm_scank": "Encontrei a próxima música:", "mm_uncache": "cache limpo; todas as músicas serão baixadas novamente na próxima reprodução", "mm_hnf": "essa música não existe mais", "im_hnf": "essa imagem não existe mais", "f_empty": 'esta pasta está vazia', "f_chide": 'isso irá ocultar a coluna «{0}»\n\nvocê pode reexibir as colunas na aba de configurações', "f_bigtxt": "este arquivo tem {0} MiB de tamanho -- realmente ver como texto?", "f_bigtxt2": "ver apenas o final do arquivo em vez disso? isso também ativará o acompanhamento/tailing, mostrando linhas de texto recém-adicionadas em tempo real", "fbd_more": '
          mostrando {0} de {1} arquivos; mostrar {2} ou mostrar todos
          ', "fbd_all": '
          mostrando {0} de {1} arquivos; mostrar todos
          ', "f_anota": "apenas {0} dos {1} itens foram selecionados;\npara selecionar a pasta inteira, primeiro role para o final", "f_dls": 'os links de arquivo na pasta atual foram\nalterados para links de download', "f_dl_nd": 'pulando pasta (use o download zip/tar em vez disso):\n', "f_partial": "Para baixar com segurança um arquivo que está sendo enviado, por favor, clique no arquivo que tem o mesmo nome, mas sem a extensão .PARTIAL. Por favor, pressione CANCELAR ou Escape para fazer isso.\n\nPressionar OK / Enter irá ignorar este aviso e continuar baixando o arquivo temporário .PARTIAL, o que quase certamente lhe dará dados corrompidos.", "ft_paste": "colar {0} itens$NHotkey: ctrl-V", "fr_eperm": 'não é possível renomear:\nvocê não tem permissão de “mover” nesta pasta', "fd_eperm": 'não é possível excluir:\nvocê não tem permissão de “excluir” nesta pasta', "fc_eperm": 'não é possível recortar:\nvocê não tem permissão de “mover” nesta pasta', "fp_eperm": 'não é possível colar:\nvocê não tem permissão de “escrever” nesta pasta', "fr_emore": "selecione pelo menos um item para renomear", "fd_emore": "selecione pelo menos um item para excluir", "fc_emore": "selecione pelo menos um item para recortar", "fcp_emore": "selecione pelo menos um item para copiar para a área de transferência", "fs_sc": "compartilhar a pasta em que você está", "fs_ss": "compartilhar os arquivos selecionados", "fs_just1d": "você não pode selecionar mais de uma pasta,\nou misturar arquivos e pastas em uma seleção", "fs_abrt": "❌ abortar", "fs_rand": "🎲 nome aleatório", "fs_go": "✅ criar compartilhamento", "fs_name": "nome", "fs_src": "fonte", "fs_pwd": "senha", "fs_exp": "expira", "fs_tmin": "min", "fs_thrs": "horas", "fs_tdays": "dias", "fs_never": "eterno", "fs_pname": "nome do link opcional; será aleatório se em branco", "fs_tsrc": "o arquivo ou pasta a ser compartilhado", "fs_ppwd": "senha opcional", "fs_w8": "criando compartilhamento...", "fs_ok": "pressione Enter/OK para Copiar para a Área de Transferência\npressione ESC/Cancelar para Fechar", "frt_dec": "pode consertar alguns casos de nomes de arquivos quebrados\">url-decode", "frt_rst": "resetar nomes de arquivos modificados de volta para os originais\">↺ resetar", "frt_abrt": "abortar e fechar esta janela\">❌ cancelar", "frb_apply": "APLICAR RENOMEAÇÃO", "fr_adv": "renomeação em lote / metadados / padrão\">avançado", "fr_case": "regex sensível a maiúsculas e minúsculas\">case", "fr_win": "nomes seguros para windows; substituir <>:"\\|?* por caracteres japoneses de largura total\">win", "fr_slash": "substituir / por um caractere que não cause a criação de novas pastas\">no /", "fr_re": "`padrão de busca regex para aplicar aos nomes de arquivos originais; grupos de captura podem ser referenciados no campo de formato abaixo como `(1)` e `(2)` e assim por diante", "fr_fmt": "`inspirado por foobar2000:$N`(título)` é substituído pelo título da música,$N`[(artista) - ](título)` pula esta parte se o artista estiver em branco$N`$lpad((tn),2,0)` preenche o número da faixa com 2 dígitos", "fr_pdel": "excluir", "fr_pnew": "salvar como", "fr_pname": "forneça um nome para seu novo preset", "fr_aborted": "abortado", "fr_lold": "nome antigo", "fr_lnew": "novo nome", "fr_tags": "tags para os arquivos selecionados (somente leitura, apenas para referência):", "fr_busy": "renomeando {0} itens...\n\n{1}", "fr_efail": "renomeação falhou:\n", "fr_nchg": "{0} dos novos nomes foram alterados devido a win e/ou no /\n\nOK para continuar com estes novos nomes alterados?", "fd_ok": "exclusão OK", "fd_err": "exclusão falhou:\n", "fd_none": "nada foi excluído; talvez bloqueado pela configuração do servidor (xbd)?", "fd_busy": "excluindo {0} itens...\n\n{1}", "fd_warn1": "EXCLUIR estes {0} itens?", "fd_warn2": "Última chance! Não há como desfazer. Excluir?", "fc_ok": "recortar {0} itens", "fc_warn": 'recortar {0} itens\n\nmas: apenas esta aba do navegador pode colá-los\n(já que a seleção é tão absolutamente massiva)', "fcc_ok": "copiado {0} itens para a área de transferência", "fcc_warn": 'copiado {0} itens para a área de transferência\n\nmas: apenas esta aba do navegador pode colá-los\n(já que a seleção é tão absolutamente massiva)', "fp_apply": "usar estes nomes", "fp_skip": "pular conflitos", "fp_ecut": "primeiro recorte ou copie alguns arquivos / pastas para colar / mover\n\nnota: você pode recortar / colar entre abas diferentes do navegador", "fp_ename": "{0} itens não podem ser movidos para cá porque os nomes já estão em uso. Dê a eles novos nomes abaixo para continuar, ou deixe o nome em branco (\"pular conflitos\") para pular:", "fcp_ename": "{0} itens não podem ser copiados para cá porque os nomes já estão em uso. Dê a eles novos nomes abaixo para continuar, ou deixe o nome em branco (\"pular conflitos\") para pular:", "fp_emore": "ainda há algumas colisões de nome de arquivo para consertar", "fp_ok": "movimento OK", "fcp_ok": "cópia OK", "fp_busy": "movendo {0} itens...\n\n{1}", "fcp_busy": "copiando {0} itens...\n\n{1}", "fp_abrt": "abortando...", "fp_err": "movimento falhou:\n", "fcp_err": "cópia falhou:\n", "fp_confirm": "mover estes {0} itens para cá?", "fcp_confirm": "copiar estes {0} itens para cá?", "fp_etab": 'falha ao ler a área de transferência de outra aba do navegador', "fp_name": "enviando um arquivo do seu dispositivo. Dê-lhe um nome:", "fp_both_m": '
          escolha o que colar
          Enter = Mover {0} arquivos de «{1}»\nESC = Enviar {2} arquivos do seu dispositivo', "fcp_both_m": '
          escolha o que colar
          Enter = Copiar {0} arquivos de «{1}»\nESC = Enviar {2} arquivos do seu dispositivo', "fp_both_b": 'MoverEnviar', "fcp_both_b": 'CopiarEnviar', "mk_noname": "digite um nome no campo de texto à esquerda antes de fazer isso :p", "nmd_i1": "também adicione a extensão desejada, por exemplo .md", "nmd_i2": "só pode criar arquivos .{0} porque não tem permissão para apagar", "tv_load": "Carregando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB carregados)", "tv_xe1": "não foi possível carregar o arquivo de texto:\n\nerro ", "tv_xe2": "404, arquivo não encontrado", "tv_lst": "lista de arquivos de texto em", "tvt_close": "voltar para a visualização da pasta$NHotkey: M (ou Esc)\">❌ fechar", "tvt_dl": "baixar este arquivo$NHotkey: Y\">💾 baixar", "tvt_prev": "mostrar documento anterior$NHotkey: i\">⬆ anterior", "tvt_next": "mostrar próximo documento$NHotkey: K\">⬇ próximo", "tvt_sel": "selecionar arquivo   ( para recortar / copiar / excluir / ... )$NHotkey: S\">sel", "tvt_j": "embelezar json$NHotkey: shift-J\">j", "tvt_edit": "abrir arquivo no editor de texto$NHotkey: E\">✏️ editar", "tvt_tail": "monitorar arquivo para alterações; mostrar novas linhas em tempo real\">📡 seguir", "tvt_wrap": "quebra de linha\">↵", "tvt_atail": "fixar rolagem no final da página\">⚓", "tvt_ctail": "decodificar cores do terminal (códigos de escape ansi)\">🌈", "tvt_ntail": "limite de rolagem para trás (quantos bytes de texto manter carregados)", "m3u_add1": "música adicionada à playlist m3u", "m3u_addn": "{0} músicas adicionadas à playlist m3u", "m3u_clip": "playlist m3u agora copiada para a área de transferência\n\nvocê deve criar um novo arquivo de texto chamado something.m3u e colar a playlist nesse documento; isso a tornará reproduzível", "gt_vau": "não mostrar vídeos, apenas tocar o áudio\">🎧", "gt_msel": "ativar seleção de arquivo; ctrl-clique em um arquivo para substituir$N$N<em>quando ativo: clique duas vezes em um arquivo / pasta para abri-lo&>t;/em>$N$NHotkey: S\">multisseleção", "gt_crop": "cortar miniaturas ao centro\">cortar", "gt_3x": "miniaturas de alta resolução\">3x", "gt_zoom": "zoom", "gt_chop": "picar", "gt_sort": "ordenar por", "gt_name": "nome", "gt_sz": "tamanho", "gt_ts": "data", "gt_ext": "tipo", "gt_c1": "truncar nomes de arquivos mais (mostrar menos)", "gt_c2": "truncar nomes de arquivos menos (mostrar mais)", "sm_w8": "buscando...", "sm_prev": "os resultados da busca abaixo são de uma consulta anterior:\n ", "sl_close": "fechar resultados da busca", "sl_hits": "mostrando {0} resultados", "sl_moar": "carregar mais", "s_sz": "tamanho", "s_dt": "data", "s_rd": "caminho", "s_fn": "nome", "s_ta": "tags", "s_ua": "up@", "s_ad": "adv.", "s_s1": "MiB mínimo", "s_s2": "MiB máximo", "s_d1": "iso8601 min.", "s_d2": "iso8601 max.", "s_u1": "enviado depois de", "s_u2": "e/ou antes de", "s_r1": "caminho contém   (separado por espaço)", "s_f1": "nome contém   (negar com -nope)", "s_t1": "tags contém   (^=início, fim=$)", "s_a1": "propriedades de metadados específicas", "md_eshow": "não é possível renderizar ", "md_off": "[📜readme] desativado em [⚙️] -- documento oculto", "badreply": "Falha ao analisar a resposta do servidor", "xhr403": "403: Acesso negado\n\ntente pressionar F5, talvez você tenha saído da conta", "xhr0": "desconhecido (provavelmente perdeu a conexão com o servidor, ou o servidor está offline)", "cf_ok": "desculpe por isso -- a proteção DD" + "oS foi ativada\n\nas coisas devem ser retomadas em cerca de 30 segundos\n\nse nada acontecer, pressione F5 para recarregar a página", "tl_xe1": "não foi possível listar as subpastas:\n\nerro ", "tl_xe2": "404: Pasta não encontrada", "fl_xe1": "não foi possível listar os arquivos na pasta:\n\nerro ", "fl_xe2": "404: Pasta não encontrada", "fd_xe1": "não foi possível criar a subpasta:\n\nerro ", "fd_xe2": "404: Pasta pai não encontrada", "fsm_xe1": "não foi possível enviar a mensagem:\n\nerro ", "fsm_xe2": "404: Pasta pai não encontrada", "fu_xe1": "falha ao carregar a lista de despublicação do servidor:\n\nerro ", "fu_xe2": "404: Arquivo não encontrado??", "fz_tar": "arquivo gnu-tar não comprimido (linux / mac)", "fz_pax": "tar de formato pax não comprimido (mais lento)", "fz_targz": "gnu-tar com compressão gzip nível 3$N$Nisto é geralmente muito lento, então$Nuse tar não comprimido em vez disso", "fz_tarxz": "gnu-tar com compressão xz nível 1$N$Nisto é geralmente muito lento, então$Nuse tar não comprimido em vez disso", "fz_zip8": "zip com nomes de arquivos utf8 (pode ser instável no windows 7 e mais antigos)", "fz_zipd": "zip com nomes de arquivos cp437 tradicionais, para software realmente antigo", "fz_zipc": "cp437 com crc32 calculado antecipadamente,$Npara MS-DOS PKZIP v2.04g (outubro de 1993)$N(leva mais tempo para processar antes que o download possa começar)", "un_m1": "você pode excluir seus uploads recentes (ou abortar os que não foram concluídos) abaixo", "un_upd": "atualizar", "un_m4": "ou compartilhar os arquivos visíveis abaixo:", "un_ulist": "mostrar", "un_ucopy": "copiar", "un_flt": "filtro opcional:  a URL deve conter", "un_fclr": "limpar filtro", "un_derr": 'a exclusão da despublicação falhou:\n', "un_f5": 'algo quebrou, por favor, tente uma atualização ou pressione F5', "un_uf5": "desculpe, mas você tem que atualizar a página (por exemplo, pressionando F5 ou CTRL-R) antes que este upload possa ser abortado", "un_nou": "aviso: o servidor está muito ocupado para mostrar uploads incompletos; clique no link \"atualizar\" em um momento", "un_noc": "aviso: a despublicação de arquivos totalmente enviados não está ativada/permitida na configuração do servidor", "un_max": "mostrando os primeiros 2000 arquivos (use o filtro)", "un_avail": "{0} uploads recentes podem ser excluídos
          {1} incompletos podem ser abortados", "un_m2": "ordenado por tempo de upload; o mais recente primeiro:", "un_no1": "sike! nenhum upload é suficientemente recente", "un_no2": "sike! nenhum upload que corresponda a esse filtro é suficientemente recente", "un_next": "excluir os próximos {0} arquivos abaixo", "un_abrt": "abortar", "un_del": "excluir", "un_m3": "carregando seus uploads recentes...", "un_busy": "excluindo {0} arquivos...", "un_clip": "{0} links copiados para a área de transferência", "u_https1": "você deveria", "u_https2": "mudar para https", "u_https3": "para um melhor desempenho", "u_ancient": 'seu navegador é impressionantemente antigo -- talvez você devesse usar o bup em vez disso', "u_nowork": "precisa do firefox 53+ ou chrome 57+ ou iOS 11+", "tail_2old": "precisa do firefox 105+ ou chrome 71+ ou iOS 14.5+", "u_nodrop": 'seu navegador é muito antigo para upload de arrastar e soltar', "u_notdir": "isso não é uma pasta!\n\nseu navegador é muito antigo,\npor favor, tente arrastar e soltar em vez disso", "u_uri": "para arrastar e soltar imagens de outras janelas do navegador,\npor favor, solte-as no grande botão de upload", "u_enpot": 'mudar para UI batata (pode melhorar a velocidade de upload)', "u_depot": 'mudar para UI chique (pode reduzir a velocidade de upload)', "u_gotpot": 'mudando para a UI batata para uma velocidade de upload melhorada,\n\nsinta-se à vontade para discordar e voltar!', "u_pott": "

          arquivos:   {0} concluídos,   {1} falhados,   {2} ocupados,   {3} na fila

          ", "u_ever": "este é o uploader básico; up2k precisa de pelo menos
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'este é o uploader básico; up2k é melhor', "u_uput": 'otimizar para velocidade (pular checksum)', "u_ewrite": 'você não tem acesso de escrita a esta pasta', "u_eread": 'você não tem acesso de leitura a esta pasta', "u_enoi": 'a busca de arquivos não está ativada na configuração do servidor', "u_enoow": "substituir não funcionará aqui; precisa de permissão de Excluir", "u_badf": 'Estes {0} arquivos (de um total de {1}) foram ignorados, possivelmente devido a permissões do sistema de arquivos:\n\n', "u_blankf": 'Estes {0} arquivos (de um total de {1}) estão em branco / vazios; enviá-los de qualquer maneira?\n\n', "u_applef": 'Estes {0} arquivos (de um total de {1}) são provavelmente indesejáveis;\nPressione OK/Enter para PULAR os seguintes arquivos,\nPressione Cancelar/ESC para NÃO excluir, e ENVIAR esses também:\n\n', "u_just1": '\nTalvez funcione melhor se você selecionar apenas um arquivo', "u_ff_many": "se você estiver usando Linux / MacOS / Android, então essa quantidade de arquivos pode travar o Firefox!\nse isso acontecer, por favor, tente novamente (ou use o Chrome).", "u_up_life": "Este upload será excluído do servidor\n{0} após ser concluído", "u_asku": 'enviar estes {0} arquivos para {1}', "u_unpt": "você pode desfazer / excluir este upload usando o 🧯 no canto superior esquerdo", "u_bigtab": 'prestes a mostrar {0} arquivos\n\nisto pode travar seu navegador, tem certeza?', "u_scan": 'Escaneando arquivos...', "u_dirstuck": 'o iterador de diretório travou ao tentar acessar os seguintes {0} itens; irá pular:', "u_etadone": 'Concluído ({0}, {1} arquivos)', "u_etaprep": '(preparando para enviar)', "u_hashdone": 'hash concluído', "u_hashing": 'hash', "u_hs": 'handshaking...', "u_started": "os arquivos estão sendo enviados agora; veja [🚀]", "u_dupdefer": "duplicado; será processado após todos os outros arquivos", "u_actx": "clique neste texto para evitar perda de
          desempenho ao mudar para outras janelas/abas", "u_fixed": "OK!  Consertado 👍", "u_cuerr": "falha ao enviar o bloco {0} de {1};\nprovavelmente inofensivo, continuando\n\narquivo: {2}", "u_cuerr2": "o servidor rejeitou o upload (bloco {0} de {1});\ntentará novamente mais tarde\n\narquivo: {2}\n\nerro ", "u_ehstmp": "tentará novamente; veja no canto inferior direito", "u_ehsfin": "o servidor rejeitou a solicitação para finalizar o upload; tentando novamente...", "u_ehssrch": "o servidor rejeitou a solicitação para realizar a busca; tentando novamente...", "u_ehsinit": "o servidor rejeitou a solicitação para iniciar o upload; tentando novamente...", "u_eneths": "erro de rede ao realizar o handshake de upload; tentando novamente...", "u_enethd": "erro de rede ao testar a existência do alvo; tentando novamente...", "u_cbusy": "esperando o servidor confiar em nós novamente após uma falha de rede...", "u_ehsdf": "o servidor ficou sem espaço em disco!\n\ncontinuará tentando novamente, caso alguém\nlibere espaço suficiente para continuar", "u_emtleak1": "parece que seu navegador pode ter um vazamento de memória;\npor favor,", "u_emtleak2": ' mude para https (recomendado) ou ', "u_emtleak3": ' ', "u_emtleakc": 'tente o seguinte:\n
          • pressione F5 para atualizar a página
          • depois desative o botão  mt  nas  ⚙️ configurações
          • e tente o upload novamente
          Os uploads serão um pouco mais lentos, mas tudo bem.\nDesculpe pelo problema !\n\nPS: chrome v107 tem uma correção de bug para isso', "u_emtleakf": 'tente o seguinte:\n
          • pressione F5 para atualizar a página
          • depois ative 🥔 (batata) na UI de upload
          • e tente o upload novamente
          \nPS: o firefox esperançosamente terá uma correção de bug em algum momento', "u_s404": "não encontrado no servidor", "u_expl": "explicar", "u_maxconn": "a maioria dos navegadores limita isso a 6, mas o firefox permite que você aumente com connections-per-server em about:config", "u_tu": '

          AVISO: turbo ativado,  o cliente pode não detectar e retomar uploads incompletos; veja a dica de ferramenta do botão turbo

          ', "u_ts": '

          AVISO: turbo ativado,  os resultados da busca podem estar incorretos; veja a dica de ferramenta do botão turbo

          ', "u_turbo_c": "o turbo está desativado na configuração do servidor", "u_turbo_g": "desativando o turbo porque você não tem\n privilégios de listagem de diretório neste volume", "u_life_cfg": 'excluir automaticamente depois de min (ou horas)', "u_life_est": 'o upload será excluído ---', "u_life_max": 'esta pasta impõe um\n tempo de vida máximo de {0}', "u_unp_ok": 'a despublicação é permitida para {0}', "u_unp_ng": 'a despublicação NÃO será permitida', "ue_ro": 'seu acesso a esta pasta é Somente Leitura\n\n', "ue_nl": 'você não está logado no momento', "ue_la": 'você está logado no momento como "{0}"', "ue_sr": 'você está no modo de busca de arquivos no momento\n\nmude para o modo de upload clicando na lupa 🔎 (ao lado do grande botão BUSCAR), e tente enviar novamente\n\ndesculpe', "ue_ta": 'tente enviar novamente, deve funcionar agora', "ue_ab": "este arquivo já está sendo enviado para outra pasta, e esse upload deve ser concluído antes que o arquivo possa ser enviado para outro lugar.\n\nVocê pode abortar e esquecer o upload inicial usando o 🧯 no canto superior esquerdo", "ur_1uo": "OK: Arquivo enviado com sucesso", "ur_auo": "OK: Todos os {0} arquivos enviados com sucesso", "ur_1so": "OK: Arquivo encontrado no servidor", "ur_aso": "OK: Todos os {0} arquivos encontrados no servidor", "ur_1un": "O upload falhou, desculpe", "ur_aun": "Todos os {0} uploads falharam, desculpe", "ur_1sn": "O arquivo NÃO foi encontrado no servidor", "ur_asn": "Os {0} arquivos NÃO foram encontrados no servidor", "ur_um": "Concluído;\n{0} uploads OK,\n{1} uploads falharam, desculpe", "ur_sm": "Concluído;\n{0} arquivos encontrados no servidor,\n{1} arquivos NÃO encontrados no servidor", "rc_opn": "abrir", "rc_ply": "reproduzir", "rc_pla": "reproduzir como áudio", "rc_txt": "abrir no leitor de texto", "rc_md": "abrir no leitor markdown", "rc_dl": "baixar", "rc_zip": "baixar compactado", "rc_cpl": "copiar link", "rc_del": "excluir", "rc_cut": "recortar", "rc_cpy": "copiar", "rc_pst": "colar", "rc_rnm": "renomear", "rc_nfo": "nova pasta", "rc_nfi": "novo arquivo", "rc_sal": "selecionar tudo", "rc_sin": "inverter seleção", "rc_shf": "compartilhar esta pasta", "rc_shs": "compartilhar seleção", "lang_set": "atualizar para a mudança ter efeito?", "splash": { "a1": "atualizar", "b1": "olá   (você não está logado)", "c1": "encerrar sessão", "d1": "despejar o estado da pilha", "d2": "mostra o estado de todos os threads ativos", "e1": "recarregar configuração", "e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito", "f1": "você pode navegar:", "g1": "você pode fazer upload para:", "cc1": "outras coisas:", "h1": "desativar k304", "i1": "ativar k304", "j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), mas também irá desacelerar as coisas em geral", "k1": "redefinir config. de cliente", "l1": "faça login para mais:", "ls3": "fazer login", "lu4": "nome de usuário", "lp4": "senha", "lo3": "encerrar sessão de \"{0}\" em todos os lugares", "lo2": "isso irá encerrar a sessão em todos os navegadores", "m1": "bem-vindo de volta,", "n1": "404 não encontrado  ┐( ´ -`)┌", "o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início", "p1": "403 proibido  ~┻━┻", "q1": "use uma senha ou volte para o início", "r1": "ir para o início", ".s1": "reescanear", "t1": "ação", "u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos", "v1": "conectar", "v2": "usar este servidor como um disco rígido local", "w1": "mudar para https", "x1": "mudar senha", "y1": "editar recursos compartilhados", "z1": "desbloquear este recurso compartilhado:", "ta1": "primeiro digite sua nova senha", "ta2": "repita para confirmar a nova senha:", "ta3": "há um erro; por favor, tente novamente", "nop": "ERRO: A senha não pode estar em branco", "nou": "ERRO: O nome de usuário e/ou a senha não podem estar em branco", "aa1": "arquivos de entrada:", "ab1": "desativar no304", "ac1": "ativar no304", "ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!", "ae1": "downloads ativos:", "af1": "mostrar uploads recentes", "ag1": "mostrar usuários IdP conhecidos", } }; ================================================ FILE: copyparty/web/tl/rus.js ================================================ // Строки, оканчивающиеся на //m, являются непроверенными машинными переводами Ls.rus = { "tt": "Русский", "cols": { "c": "кнопки действий", "dur": "длительность", "q": "качество / битрейт", "Ac": "аудио кодек", "Vc": "видео кодек", "Fmt": "формат / контейнер", "Ahash": "контрольная сумма аудио", "Vhash": "контрольная сумма видео", "Res": "разрешение", "T": "тип файла", "aq": "качество аудио / битрейт", "vq": "качество видео / битрейт", "pixfmt": "сабсемплинг / пиксельный формат", "resw": "горизонтальное разрешение", "resh": "вертикальное разрешение", "chs": "аудио каналы", "hz": "частота дискретизации", }, "hks": [ [ "разное", ["ESC", "закрыть всякие штуки"], "файловый менеджер", ["G", "переключиться между списком / плиткой"], ["T", "переключиться между миниатюрами / иконками"], ["⇧ A/D", "размер миниатюры"], ["ctrl-K", "удалить выделенное"], ["ctrl-X", "вырезать выделенное в буфер"], ["ctrl-C", "копировать выделенное в буфер"], ["ctrl-V", "вставить (переместить/копировать) сюда"], ["Y", "скачать выделенное"], ["F2", "переименовать выделенное"], "выделение файлов", ["пробел", "выделить/снять выделение с текущего файла"], ["↑/↓", "двигать курсор"], ["ctrl ↑/↓", "двигать курсор и скроллить"], ["⇧ ↑/↓", "выделить предыдущий/следующий файл"], ["ctrl-A", "выделить все файлы и папки"], ], [ "навигация", ["B", "показать/скрыть панель навигации"], ["I/K", "предыдущая/следующая папка"], ["M", "перейти на уровень выше (или свернуть текущую папку)"], ["V", "переключиться между папками / текстовыми файлами в панели навигации"], ["A/D", "уменьшить/увеличить панель навигации"], ], [ "аудиоплеер", ["J/L", "предыдущий/следующий трек"], ["U/O", "перемотать на 10 секунд назад/вперёд"], ["0..9", "перемотать на 0%..90%"], ["P", "играть/пауза (или подгрузить трек)"], ["S", "выделить текущий трек"], ["Y", "скачать трек"], ], [ "просмотрщик изображений", ["J/L, ←/→", "предыдущее/следующее изображение"], ["Home/End", "первое/последнее изображение"], ["F", "развернуть на полный экран"], ["R", "повернуть по часовой стрелке"], ["⇧ R", "повернуть против часовой стрелки"], ["S", "выделить изображение"], ["Y", "скачать изображение"], ], [ "видеоплеер", ["U/O", "перемотать на 10 секунд назад/вперёд"], ["P/K/Space", "играть/пауза"], ["C", "продолжить проигрывать следующее"], ["V", "повтор"], ["M", "выключить звук"], ["[ and ]", "задать интервал повтора"], ], [ "просмотрщик текстовых файлов", ["I/K", "предыдущий/следующий файл"], ["M", "закрыть файл"], ["E", "отредактировать файл"], ["S", "выделить файл"], ["Y", "скачать текстовый файл"], //m ["⇧ J", "приукрасить json"], //m ] ], "m_ok": "OK", "m_ng": "Отмена", "enable": "Включить", "danger": "ВНИМАНИЕ", "clipped": "скопировано в буфер обмена", "ht_s1": "секунда", "ht_s2": "секунды", "ht_m1": "минута", "ht_m2": "минуты", "ht_h1": "час", "ht_h2": "часы", "ht_d1": "день", "ht_d2": "дни", "ht_and": " и ", "goh": "панель управления", "gop": 'предыдущая папка">пред', "gou": 'родительская папка">вверх', "gon": 'следующая папка">след', "logout": "Выйти ", "login": "Войти", //m "access": " доступ", "ot_close": "закрыть подменю", "ot_search": "`искать файлы по атрибутам, пути / имени, музыкальным тегам или любой другой комбинации из следующих конструкций$N$N`foo bar` = обязано содержать «foo» И «bar»,$N`foo -bar` = обязано содержать «foo», но не «bar»,$N`^yana .opus$` = начинается с «yana» и имеет расширение «opus»$N`"try unite"` = содержит именно «try unite»$N$Nформат времени задаётся по стандарту iso-8601, например$N`2009-12-31` или `2020-09-12 23:30:00`", "ot_unpost": "unpost: удалить ваши недавние загрузки и отменить незавершённые", "ot_bup": "bup: легковесный загрузчик файлов, поддерживает даже netscape 4.0", "ot_mkdir": "mkdir: создать новую папку", "ot_md": "new-file: создать новый текстовый файл", //m "ot_msg": "msg: отправить сообщение в лог сервера", "ot_mp": "настройка медиаплеера", "ot_cfg": "остальные настройки", "ot_u2i": 'up2k: загрузить файлы (если имеется доступ к записи) или переключиться в режим поиска$N$Nзагрузки являются возобновляемыми и многопоточными, а даты изменения файлов сохраняются в процессе, но этот метод использует больше ресурсов процессора, чем [🎈]  (легковесный загрузчик)

          во время загрузки эта иконка превращается в индикатор!', "ot_u2w": 'up2k: загрузить файлы с поддержкой возобновления (закиньте те же файлы после перезапуска браузера)$N$Nподдерживается многопоточность, даты изменения файлов сохраняются в процессе, но этот метод использует больше ресурсов процессора, чем [🎈]  (легковесный загрузчик)

          во время загрузки эта иконка превращается в индикатор!', "ot_noie": 'Пожалуйста, используйте Chrome / Firefox / Edge', "ab_mkdir": "создать папку", "ab_mkdoc": "создать текстовый файл", //m "ab_msg": "отправить сообщение в лог сервера", "ay_path": "перейти к папкам", "ay_files": "перейти к файлам", "wt_ren": "переименовать выделенные файлы$NГорячая клавиша: F2", "wt_del": "удалить выделенные файлы$NГорячая клавиша: ctrl-K", "wt_cut": "вырезать выделенные файлы <small>(затем вставить куда-то в другое место)</small>$NГорячая клавиша: ctrl-X", "wt_cpy": "копировать выделенные файлы в буфер$N(чтобы вставить их куда-то ещё)$NГорячая клавиша: ctrl-C", "wt_pst": "вставить ранее вырезанный / скопированный файл$NГорячая клавиша: ctrl-V", "wt_selall": "выделить все файлы$NГорячая клавиша: ctrl-A (когда выделен хотя бы один файл)", "wt_selinv": "инвертировать выделение", "wt_zip1": "скачать эту папку как архив", "wt_selzip": "скачать выделенные файлы как архив", "wt_seldl": "скачать выделенные файлы по-отдельности$NГорячая клавиша: Y", "wt_npirc": "копировать информацию о треке в формате irc", "wt_nptxt": "копировать информацию о треке обычным текстом", "wt_m3ua": "добавить в плейлист m3u (нажмите 📻коп. в конце)", "wt_m3uc": "копировать плейлист m3u в буфер обмена", "wt_grid": "переключить между сеткой / списком$NГорячая клавиша: G", "wt_prev": "предыдущий трек$NГорячая клавиша: J", "wt_play": "играть / пауза$NГорячая клавиша: P", "wt_next": "следующий трек$NГорячая клавиша: L", "ul_par": "параллельные загрузки:", "ut_rand": "случайные имена файлов", "ut_u2ts": "копировать время последнего изменения$Nиз вашей файловой системы на сервер\">📅", "ut_ow": "перезаписывать существующие файлы на сервере?$N🛡️: нет (для повторяющихся файлов будут создаваться новые имена)$N🕒: перезаписать файлы с датой изменения старее, чем у загружаемых$N♻️: всегда перезаписывать (если файлы различаются по содержанию)$N⏭️: безусловно пропускать все существующие файлы", //m "ut_mt": "продолжать хешировать другие файлы во время загрузки$N$Nесть смысл отключить при медленном диске или процессоре", "ut_ask": 'требовать подтверждения перед началом загрузки">💭', "ut_pot": "улучшить скорость загрузки на слабых устройства$Nс помощью упрощения интерфейса", "ut_srch": "не загружать, а проверять, существуют ли данные файлы $N на сервере (проверка всех доступных вам папок)", "ut_par": "при 0 загрузка встанет на паузу$N$Nследует повысить, если ваше подключение медленное$N$Nоставьте 1, если используется локальная сеть или диск сервера медленный", "ul_btn": "отпустите файлы / папки
          здесь (или нажмите)", "ul_btnu": "З А Г Р У З И Т Ь", "ul_btns": "И С К А Т Ь", "ul_hash": "хеш", "ul_send": "отправка", "ul_done": "готово", "ul_idle1": "нет загрузок в очереди", "ut_etah": "средняя скорость <em>хеширования</em> и примерное время до завершения", "ut_etau": "средняя скорость <em>загрузки</em> и примерное время до завершения", "ut_etat": "средняя <em>общая</em> скорость и примерное время до завершения", "uct_ok": "успешно завершены", "uct_ng": "ошибки / отказы / не найдены", "uct_done": "готово (ok и ng вместе)", "uct_bz": "хешируются или загружаются", "uct_q": "в очереди", "utl_name": "имя файла", "utl_ulist": "список", "utl_ucopy": "копировать", "utl_links": "ссылки", "utl_stat": "статус", "utl_prog": "прогресс", // keep short: "utl_404": "404", "utl_err": "ОШИБКА", "utl_oserr": "ошибка ОС", "utl_found": "найдено", "utl_defer": "отложить", "utl_yolo": "турбо", "utl_done": "готово", "ul_flagblk": "файлы были добавлены в очередь,
          однако в другой вкладке уже есть активная загрузка через up2k,
          поэтому ожидаем её завершения", "ul_btnlk": "настройки сервера запрещают изменение состояния этой опции", "udt_up": "Загрузить", "udt_srch": "Поиск", "udt_drop": "отпустите здесь", "u_nav_m": '
          лады, что там у вас?
          Enter = Файлы (один или больше)\nESC = Одна папка (с учётом подпапок)', "u_nav_b": 'ФайлыОдна папка', "cl_opts": "переключатели", "cl_hfsz": "размер файла", //m "cl_themes": "тема", "cl_langs": "язык", "cl_ziptype": "архивация папок", "cl_uopts": "опции up2k", "cl_favico": "иконка", "cl_bigdir": "бол. папки", "cl_hsort": "#сорт.", "cl_keytype": "схема горячих клавиш", "cl_hiddenc": "скрытые столбцы", "cl_hidec": "скрыть", "cl_reset": "сбросить", "cl_hpick": "нажмите на заголовки столбцов, чтобы скрыть их в таблице ниже", "cl_hcancel": "скрытие столбца отменено", "cl_rcm": "контекстное меню", //m "ct_grid": '田 сетка', "ct_ttips": '◔ ◡ ◔">ℹ️ подсказки', "ct_thumb": 'переключение между иконками и миниатюрами в режиме сетки$NГорячая клавиша: T">🖼️ миниат.', "ct_csel": 'держите CTRL или SHIFT для выделения файлов в режиме сетки">выбор', "ct_dsel": 'использовать выделение перетаскиванием в режиме сетки">перетащить', //m "ct_dl": 'принудительная загрузка (не показывать встроенно) при щелчке по файлу">dl', //m "ct_ihop": 'показывать последний открытый файл после закрытия просмотрщика изображений">g⮯', "ct_dots": 'показывать скрытые файлы (если есть доступ)">скрыт.', "ct_qdel": 'спрашивать подтверждение только один раз перед удалением файлов">быстр. удал.', "ct_dir1st": 'разместить папки над файлами">📁 сверху', "ct_nsort": 'сортировка по числам$N(например, файл с >code<2>/code< в начале названия идёт перед >code<11>/code<)">нат. сорт.', "ct_utc": 'используйте UTC для всех временных меток">UTC', //m "ct_readme": 'показывать содержимое README.md в описании папки">📜 ридми', "ct_idxh": 'показывать страницу index.html в текущей папке вместо интерфейса">htm', "ct_sbars": 'показывать полосы прокрутки">⟊', "cut_umod": "если файл уже существует на сервере, обновить время последнего изменения на сервере в соответствии с локальным файлом (требуются права write+delete)\">перенос 📅", "cut_turbo": "используйте эту функцию С ОСТОРОЖНОСТЬЮ:$N$Nпригодится в случае, если была прервана загрузка большого количества файлов, и вы хотите возобновить её как можно быстрее$N$Nпроверка файлов по хешу заменяется на простой алгоритм: "если размер файла отличается - тогда загрузить", но содержание файлов не сравнивается$N$Nследует отключить эту функцию после окончания загрузки, а затем "загрузить" те же файлы заново, чтобы валидировать их\">турбо", "cut_datechk": "работает только при включённой кнопке "турбо"$N$Nчуть-чуть повышает надёжность турбо-загрузок с помощью сверки дат изменений между файлами на сервере и вашими$N$Nв теории достаточно для проверки большинства незавершённых / повреждённых загрузок, но не является альтернативой валидации файлов после турбо-загрузки\">провер. дат", "cut_u2sz": "размер (в МиБ) каждой загружаемой части; большие значения показывают лучшие результаты для дальних соединений. Если подключение нестабильное, попробуйте значение пониже", "cut_flag": "разрешить одновременную загрузку только из одной вкладки за раз $N -- обязательно включить эту опцию в остальных вкладках $N -- работает только в пределах одного домена", "cut_az": "загружать файлы в алфавитном порядке вместо "от меньшего к большему"$N$Nэто позволит проще отследить проблемы во время загрузки, но скорость слегка ниже на очень быстрых соединениях (например, в локальной сети)", "cut_nag": "системное уведомление по завершении загрузки$N(только при неактивной вкладке браузера)", "cut_sfx": "звуковое уведомление по завершении загрузки$N(только при неактивной вкладке браузера)", "cut_mt": "использовать многопоточность для ускорения хеширования$N$Nиспользует Web Worker'ы и требует больше памяти (до 512 МиБ)$N$Nускоряет https на 30%, http - в 4,5 раз\">мп", "cut_wasm": "использовать модуль WASM вместо встроенной в браузер функции хеширования; ускоряет процесс в браузерах на основе Chromium, но увеличивает нагрузку на процессор. Старые версии Chrome содержат баги, которые заполняют всю оперативную память и крашат браузер, когда включена эта опция\">wasm", "cft_text": "текст для иконки (очистите поле и перезагрузите страницу для применения)", "cft_fg": "цвет текста", "cft_bg": "цвет фона", "cdt_lim": "максимальное количество файлов для показа в папке", "cdt_ask": "внизу страницы спрашивать о действии вместо автоматической загрузки следующих файлов", "cdt_hsort": "`сколько правил сортировки (`,sorthref`) включать в адрес страницы. Если значение равно 0, по нажатии на ссылки будут игнорироваться правила, включённые в них", "cdt_ren": "включить настраиваемое контекстное меню, обычное меню доступно при нажатии shift и правой кнопки мыши\">вкл.", "cdt_rdb": "показывать обычное меню правого клика, если пользовательское уже открыто и выполняется повторный клик\">двойное", "tt_entree": "показать панель навигации$NГорячая клавиша: B", "tt_detree": "скрыть панель навигации$NГорячая клавиша: B", "tt_visdir": "прокрутить до выделенной папки", "tt_ftree": "переключить между иерархией и списком текстовых файлов$NГорячая клавиша: V", "tt_pdock": "закрепить родительские папки сверху панели", "tt_dynt": "автоматическое расширение панели", "tt_wrap": "перенос слов", "tt_hover": "раскрывать обрезанные строки при наведении$N( ломает скроллинг, если $N  курсор не в пустоте слева )", "ml_pmode": "в конце папки...", "ml_btns": "команды", "ml_tcode": "транскодировать", "ml_tcode2": "транскод. в", "ml_tint": "затемн.", "ml_eq": "эквалайзер", "ml_drc": "компрессор", "ml_ss": "пропускать тишину", //m "mt_loop": "повторять один трек\">🔁", "mt_one": "остановить после этого трека\">1️⃣", "mt_shuf": "перемешать треки во всех папках\">🔀", "mt_aplay": "автоматически играть треки по нажатии на ссылки с их ID$N$Nпри отключении адрес сайта также перестанет обновляться в соответствии с текущим треком\">a▶", "mt_preload": "подгружать следующий трек перед концом текущего для бесшовного переключения\">предзагр.", "mt_prescan": "переходить в следующую папку перед окончанием последнего трека$Nне даёт браузеру прервать следующий плейлист\">нав.", "mt_fullpre": "подгружать следующий трек целиком;$N✅ полезно при нестабильном подключении,$N❌ при медленной скорости лучше выключить\">цел.", "mt_fau": "для телефонов: начинать следующий трек сразу, даже если он не успел подгрузиться целиком (может сломать отображение тегов)\">☕️", "mt_waves": "визуализация:$Nпоказывать волну громкости на полосе воспроизведения\">~", "mt_npclip": "показать кнопки копирования для текущего трека\">/np", "mt_m3u_c": "показать кнопки копирования для выделенных треков$Nв формате плейлистов m3u8\">📻", "mt_octl": "интеграция с ОС (поддержка медиа-клавиш и музыкальных виджетов)\">интегр.", "mt_oseek": "позволить перематывать треки через системные виджеты$N$Nвнимание: на некоторых устройствах (iPhone)$Nэто заменит кнопку следующего трека\">перемотка", "mt_oscv": "показывать картинки альбомов в виджетах\">арт", "mt_follow": "держать фокус на играющем треке\">🎯", "mt_compact": "компактный плеер\">⟎", "mt_uncache": "очистить кеш  (если браузер кешировал повреждённый$Nтрек и отказывается его запускать)\">уд. кеш", "mt_mloop": "повторять треки в папке\">🔁 цикл", "mt_mnext": "загрузить следующую папку и продолжить в ней\">📂 след.", "mt_mstop": "приостановить воспроизведение\">⏸ стоп", "mt_cflac": "конвертировать flac / wav в {0}\">flac", "mt_caac": "конвертировать aac / m4a в {0}\">aac", "mt_coth": "конвертировать всё остальное (кроме mp3) в {0}\">др.", "mt_c2opus": "лучший вариант для компьютеров и устройств на Android\">opus", "mt_c2owa": "opus-weba, для iOS 17.5 и выше\">owa", "mt_c2caf": "opus-caf, для iOS 11-17\">caf", "mt_c2mp3": "для очень старых устройств\">mp3", "mt_c2flac": "лучшее качество звука, но большие файлы\">flac", //m "mt_c2wav": "не сжатое воспроизведение (ещё больше)\">wav", //m "mt_c2ok": "хороший выбор", "mt_c2nd": "это не рекомендованный вариант формата для вашего устройства, но сойдёт", "mt_c2ng": "не похоже, что ваше устройство поддерживает этот формат, но давайте попробуем и узнаем наверняка", "mt_xowa": "в iOS есть баги, препятствующие фоновому воспроизведению этого формата. Пожалуйста, используйте caf или mp3", "mt_tint": "непрозрачность фона (0-100) на полосе воспроизведения$N$Nделает буферизацию менее отвлекающей", "mt_eq": "`включить эквалайзер$N$Nboost `0` = стандартная громкость$N$Nwidth `1  ` = обычное стерео$Nwidth `0.5` = микширование левого и правого каналов на 50%$Nwidth `0  ` = моно$N$Nboost `-0.8` и width `10` = удаление голоса :^)$N$Nвключённый эквалайзер полностью убирает задержку между треками, поэтому следует его включить со всеми значениями на 0 (кроме width = 1), если вам нужно бесшовное воспроизведение", "mt_drc": "включить компрессор; также включит эквалайзер для баланса вселенной, так что выставьте всё на 0 кроме width, если он вам не нужен$N$Nпонижает громкость при волне выше значения dB в tresh; каждый dB в ratio равен одному dB на выходе, так что стандартные значения tresh = -24 и ratio = 12 сделают так, что звук никогда не будет громче -22 dB. При таком раскладе можно поставить boost в эквалайзере на 0.8 или даже 1.8 при значении atk = 0 и огромном rls вроде 90 (работает только в Firefox, rls не может быть выше 1 в других браузерах)$N$N(загляните в википедию, там всё объяснено подробнее)", "mt_ss": "`включает пропуск тишины; умножает скорость на `уск` возле начала/конца, когда громкость ниже `гром` и позиция в первых `нач`% или последних `кон`%", //m "mt_ssvt": "порог громкости (0-255)\">гром", //m "mt_ssts": "активный порог (% трека, начало)\">нач", //m "mt_sste": "активный порог (% трека, конец)\">кон", //m "mt_sssm": "множитель скорости воспроизведения\">уск", //m "mb_play": "играть", "mm_hashplay": "воспроизвести этот музыкальный файл?", "mm_m3u": "нажмите Enter/OK, чтобы играть\nнажмите ESC/Отмена, чтобы редактировать", "mp_breq": "требуется Firefox 82+, Chrome 73+ или iOS 15+", "mm_bload": "загружаю...", "mm_bconv": "конвертирую в {0}, подождите...", "mm_opusen": "ваш браузер не может воспроизводить файлы aac / m4a;\nвключено транскодирование в opus", "mm_playerr": "ошибка воспроизведения: ", "mm_eabrt": "Попытка воспроизведения была отменена", "mm_enet": "Ваше подключение нестабильно", "mm_edec": "Этот файл, возможно, повреждён??", "mm_esupp": "Ваш браузер не распознаёт этот аудио-формат", "mm_eunk": "Неопознанная ошибка", "mm_e404": "Не удалось воспроизвести аудио; ошибка 404: Файл не найден.", "mm_e403": "Не удалось воспроизвести аудио; ошибка 403: Доступ запрещён.\n\nПопробуйте перезагрузить страницу, возможно, ваша сессия истекла", "mm_e415": "Не удалось воспроизвести аудио; ошибка 415: Сбой преобразования файла; проверьте логи сервера.", //m "mm_e500": "Не удалось воспроизвести аудио; ошибка 500: Проверьте логи сервера.", "mm_e5xx": "Не удалось воспроизвести аудио; ошибка сервера ", "mm_nof": "больше аудио-файлов не найдено", "mm_prescan": "Поиск музыки для воспроизведения дальше...", "mm_scank": "Найден следующий трек:", "mm_uncache": "кеш очищен; все треки будут загружены заново при воспроизведении", "mm_hnf": "это трек больше не существует", "im_hnf": "это изображение больше не существует", "f_empty": 'эта папка пуста', "f_chide": 'это скроет столбец «{0}»\n\nвы можете показать скрытые столбцы в настройках', "f_bigtxt": "объём данного файла - {0} МиБ. точно открыть как текст?", "f_bigtxt2": "просмотреть только конец файла? это также включит обновление в реальном времени, показывая новые строки сразу после их добавления", "fbd_more": '
          показано {0} из {1} файлов; показать {2} или показать всё
          ', "fbd_all": '
          показано {0} из {1} файлов; показать всё
          ', "f_anota": "только {0} из {1} файлов было выделено;\nчтобы выделить всё папку, отмотайте до низа", "f_dls": 'ссылки на файлы в данной папке были\nзаменены ссылками на скачивание', "f_dl_nd": 'пропуск папки (используйте загрузку zip/tar вместо этого):\n', //m "f_partial": "Чтобы безопасно скачать файл, который в текущий момент загружается, нажмите на файл с таким же названием, но без расширения .PARTIAL. Пожалуйста, нажмите Отмена или ESC, чтобы сделать это.\n\nПри нажатии OK / Enter, вы скачаете этот временный файл, который с огромной вероятностью содержит лишь неполные данные.", "ft_paste": "вставить {0} файлов$NГорячая клавиша: ctrl-V", "fr_eperm": 'не удалось переименовать:\nу вас нет разрешения “move” в этой папке', "fd_eperm": 'не удалось удалить:\nу вас нет разрешения “delete” в этой папке', "fc_eperm": 'не удалось вырезать:\nу вас нет разрешения “move” в этой папке', "fp_eperm": 'не удалось вставить:\nу вас нет разрешения “write” в этой папке', "fr_emore": "выделите хотя бы один файл, чтобы переименовать", "fd_emore": "выделите хотя бы один файл, чтобы удалить", "fc_emore": "выделите хотя бы один файл, чтобы вырезать", "fcp_emore": "выделите хотя бы один файл, чтобы скопировать в буфер", "fs_sc": "поделиться текущей папкой", "fs_ss": "поделиться выделенными файлами", "fs_just1d": "вы не можете выбрать больше одной папки\nили смешивать файлы с папками при выделении", "fs_abrt": "❌ отменить", "fs_rand": "🎲 случ. имя", "fs_go": "✅ создать доступ", "fs_name": "имя", "fs_src": "путь", "fs_pwd": "пароль", "fs_exp": "срок", "fs_tmin": "мин", "fs_thrs": "часов", "fs_tdays": "дней", "fs_never": "вечно", "fs_pname": "имя ссылки; генерируется случайно если не указано", "fs_tsrc": "путь к файлу или папке, которыми нужно поделиться", "fs_ppwd": "пароль (необязательно)", "fs_w8": "создаю доступ...", "fs_ok": "нажмите Enter/OK, чтобы скопировать\nнажмите ESC/Отмена, чтобы закрыть", "frt_dec": "может исправить некоторые случаи с некорректными именами файлов\">декодировать url", "frt_rst": "сбросить изменённые имена обратно к оригинальным\">↺ сброс", "frt_abrt": "отменить операцию и закрыть это окно\">❌ отмена", "frb_apply": "ПЕРЕИМЕНОВАТЬ", "fr_adv": "переименование массовое / метаданных / по шаблону\">эксперт", "fr_case": "чувствительный к регистру regex\">РеГиСтР", "fr_win": "совместимые с windows имена; заменяет <>:"\\|?* японскими полноширинными символами\">win", "fr_slash": "заменяет / символом, который не создаёт новые папки\">без /", "fr_re": "`поиск по шаблону regex, применяемый к оригинальным именам; группы захвата могут применяться в поле форматирования с помощью `(1)` и `(2)` и так далее", "fr_fmt": "`вдохновлено foobar2000:$N`(title)` заменяется названием трека,$N`[(artist) - ](title)` пропускает [эту] часть, если композитор не указан$N`$lpad((tn),2,0)` добавляет ведущие нули до двух цифр", "fr_pdel": "удалить", "fr_pnew": "сохранить как", "fr_pname": "предоставьте название для нового шаблона", "fr_aborted": "прервано", "fr_lold": "старое имя", "fr_lnew": "новое имя", "fr_tags": "теги для выделенных файлов (не редактируется, это для инструкции):", "fr_busy": "переименовываю {0} файлов...\n\n{1}", "fr_efail": "ошибка переименования:\n", "fr_nchg": "{0} новых имён были модифицированы для соответствия опциям win и/или без /\n\nХотите использовать эти имена?", "fd_ok": "успешно удалено", "fd_err": "ошибка удаления:\n", "fd_none": "ничего не удалено; возможно, не позволяет конфигурация сервера (xbd)?", "fd_busy": "удалено {0} файлов...\n\n{1}", "fd_warn1": "УДАЛИТЬ эти {0} файлов?", "fd_warn2": "Внимание! Это необратимый процесс. Удалить?", "fc_ok": "вырезано {0} файлов", "fc_warn": 'вырезано {0} файлов\n\nно только эта вкладка браузера может их вставить\n(поскольку выделение оказалось настолько огромным)', "fcc_ok": "скопировано {0} файлов в буфер", "fcc_warn": 'скопировано {0} файлов в буфер\n\nно только эта вкладка браузера может их вставить\n(поскольку выделение оказалось настолько огромным)', "fp_apply": "использовать эти имена", "fp_skip": "пропустить конфликты", //m "fp_ecut": "сначала вырезать или скопировать только некоторые файлы / папки\n\nучтите: вы можете вырезать / вставлять файлы между вкладками", "fp_ename": "{0} файлов невозможно перенести сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\"пропустить конфликты\"), чтобы пропустить:", //m "fcp_ename": "{0} файлов невозможно скопировать сюда, потому что их имена уже заняты. Введите имена ниже, чтобы продолжить, или оставьте поля пустыми (\"пропустить конфликты\"), чтобы пропустить:", //m "fp_emore": "есть ещё коллизии имён, которые требуется исправить", "fp_ok": "успешно перенесено", "fcp_ok": "успешно скопировано", "fp_busy": "перемещаю {0} файлов...\n\n{1}", "fcp_busy": "копирую {0} файлов...\n\n{1}", "fp_abrt": "прерывание...", //m "fp_err": "ошибка перемещения:\n", "fcp_err": "ошибка копирования:\n", "fp_confirm": "переместить эти {0} файлов сюда?", "fcp_confirm": "скопировать эти {0} файлов сюда?", "fp_etab": 'ошибка чтения буфера обмена из другой вкладки браузера', "fp_name": "загружаю файл с вашего устройства. Назовите его:", "fp_both_m": '
          выберите, что вставить
          Enter = Перенести {0} файлов из «{1}»\nESC = Загрузить {2} файлов с вашего устройства', "fcp_both_m": '
          выберите, что вставить
          Enter = Скопировать {0} файлов из «{1}»\nESC = Загрузить {2} файлов с вашего устройства', "fp_both_b": 'ПереместитьЗагрузить', "fcp_both_b": 'СкопироватьЗагрузить', "mk_noname": "введите имя в текстовое поле слева перед тем, как это делать :p", "nmd_i1": "вы также можете указать нужное расширение, например .md", //m "nmd_i2": "вы можете создавать только файлы .{0}, так как у вас нет разрешения на удаление", //m "tv_load": "Загружаю текстовый документ:\n\n{0}\n\n{1}% ({2} из {3} МиБ загружено)", "tv_xe1": "не удалось загрузить текстовый файл:\n\nошибка ", "tv_xe2": "404, файл не найден", "tv_lst": "список текстовых файлов в", "tvt_close": "вернуться в обзор папки$NГорячая клавиша: M (или Esc)\">❌ закрыть", "tvt_dl": "скачать этот файл$NГорячая клавиша: Y\">💾 скачать", "tvt_prev": "показать предыдущий документ$NГорячая клавиша: i\">⬆ пред", "tvt_next": "показать следующий документ$NГорячая клавиша: K\">⬇ след", "tvt_sel": "выбрать документ   ( для вырезания / копирования / удаления / ... )$NГорячая клавиша: S\">выд", "tvt_j": "приукрасить json$NГорячая клавиша: shift-J\">j", //m "tvt_edit": "открыть документ в текстовом редакторе$NГорячая клавиша: E\">✏️ изменить", "tvt_tail": "проверять файл на изменения; показывать новые строки в реальном времени\">📡 обновлять", "tvt_wrap": "перенос слов\">↵", "tvt_atail": "прикрепить вид к низу страницы\">⚓", "tvt_ctail": "декодировать цвета терминала (ansi escape codes)\">🌈", "tvt_ntail": "лимит прокрутки (как много байт текста держать в памяти)", "m3u_add1": "трек добавлен в плейлист m3u", "m3u_addn": "{0} треков добавлено в плейлист m3u", "m3u_clip": "плейлист m3u скопирован в буфер\n\nсоздайте файл с расширением .m3u и вставьте текст туда, чтобы сделать из него плейлист", "gt_vau": "не показывать видео, только воспроизводить аудио\">🎧", "gt_msel": "включить режим выделения; держите ctrl при нажатии для инвертации действия$N$N<em>когда активно: дважды кликните на файле / папке, чтобы открыть их</em>$N$NГорячая клавиша: S\">выделение", "gt_crop": "обрезать миниатюры\">обрезка", "gt_3x": "миниатюры высокого разрешения\">3x", "gt_zoom": "размер", "gt_chop": "длина имён", "gt_sort": "сортировать по", "gt_name": "имени", "gt_sz": "размеру", "gt_ts": "дате", "gt_ext": "типу", "gt_c1": "укоротить названия файлов", "gt_c2": "удлинить названия файлов", "sm_w8": "ищем...", "sm_prev": "результаты поиска ниже - из предыдущего запроса:\n ", "sl_close": "закрыть результаты поиска", "sl_hits": "показ {0} совпадений", "sl_moar": "загрузить больше", "s_sz": "размер", "s_dt": "дата", "s_rd": "путь", "s_fn": "имя", "s_ta": "теги", "s_ua": "дата⬆️", "s_ad": "другое", "s_s1": "минимум МиБ", "s_s2": "максимум МиБ", "s_d1": "мин. iso8601", "s_d2": "макс. iso8601", "s_u1": "загружено после", "s_u2": "и/или до", "s_r1": "путь содержит   (разделить пробелами)", "s_f1": "имя содержит   (для исключения писать -nope)", "s_t1": "теги содержат   (^=начало, конец=$)", "s_a1": "свойства метаданных", "md_eshow": "не удалось показать ", "md_off": "[📜ридми] отключён в [⚙️] -- документ скрыт", "badreply": "Ошибка обработки ответа сервера", "xhr403": "403: Доступ запрещён\n\nпопробуйте перезагрузить страницу, возможно, ваша сессия истекла", "xhr0": "неизвестно (возможно, потеряно соединение с сервером, либо он отключён)", "cf_ok": "просим прощения -- сработала защита от DD" + wah + "oS\n\nвсё должно вернуться в норму через 30 сек\n\nесли ничего не происходит - перезагрузите страницу", "tl_xe1": "не удалось показать подпапки:\n\nошибка ", "tl_xe2": "404: Папка не найдена", "fl_xe1": "не удалось показать файлы:\n\nошибка ", "fl_xe2": "404: Папка не найдена", "fd_xe1": "не удалось создать подпапку:\n\nошибка ", "fd_xe2": "404: Родительская папка не найдена", "fsm_xe1": "не удалось отправить сообщение:\n\nошибка ", "fsm_xe2": "404: Родительская папка не найдена", "fu_xe1": "не удалось удалить список с сервера:\n\nошибка ", "fu_xe2": "404: Файл не найден??", "fz_tar": "несжатый файл gnu-tar (linux / mac)", "fz_pax": "несжатый pax-форматированный tar (медленнее)", "fz_targz": "gnu-tar с 3 уровнем сжатия gzip$N$Nобычно это очень медленно,$Nлучше использовать несжатый tar", "fz_tarxz": "gnu-tar с 1 уровнем сжатия xz$N$Nобычно это очень медленно,$Nлучше использовать несжатый tar", "fz_zip8": "zip с именами по utf8 (может работать криво на windows 7 и ниже)", "fz_zipd": "zip с именами по cp437, для очень старого софта", "fz_zipc": "cp437 с предварительным вычислением crc32,$N для MS-DOS PKZIP v2.04g (октябрь 1993)$N(требует больше времени для обработки перед скачиванием)", "un_m1": "вы можете удалить ваши недавние загрузки (или отменить незавершённые) ниже", "un_upd": "обновить", "un_m4": "или поделиться файлами снизу:", "un_ulist": "показать", "un_ucopy": "копировать", "un_flt": "опциональный фильтр:  адрес должен содержать", "un_fclr": "очистить фильтр", "un_derr": 'ошибка удаления:\n', "un_f5": 'что-то сломалось, пожалуйста перезагрузите страницу', "un_uf5": "извините, но вам нужно перезагрузить страницу (F5 или Ctrl+R) перед тем, как отменить эту загрузку", "un_nou": 'внимание: сервер слишком нагружен, чтобы показать незавершённые загрузки; нажмите на ссылку "обновления" через пару секунд', "un_noc": 'внимание: удаление уже загруженных файлов запрещено конфигурацией сервера', "un_max": "показаны первые 2000 файлов (используйте фильтр)", "un_avail": "{0} недавних загрузок может быть удалено
          {1} незавершённых может быть отменено", "un_m2": "отсортировано по времени загрузки; сначала самые последние:", "un_no1": "ха, поверил! достаточно свежих загрузок ещё нет", "un_no2": "ха, поверил! достаточно свежих загрузок, соответствующих фильтру, ещё нет", "un_next": "удалить следующие {0} файлов ниже", "un_abrt": "отменить", "un_del": "удалить", "un_m3": "загружаю ваши недавние загрузки...", "un_busy": "удаляю {0} файлов...", "un_clip": "{0} ссылок скопировано в буфер", "u_https1": "вам стоит", "u_https2": "включить https", "u_https3": "для лучшей производительности", "u_ancient": 'у вас действительно антикварный браузер -- возможно, стоит использовать bup', "u_nowork": "требуется firefox 53+, chrome 57+ или iOS 11+", "tail_2old": "требуется firefox 105+, chrome 71+ или iOS 14.5+", "u_nodrop": 'ваш браузер слишком старый для загрузки через перетаскивание', "u_notdir": "это не папка!\n\nваш браузер слишком старый,\nиспользуйте перетаскивание", "u_uri": "чтобы перетащить картинку из других окон браузера,\nотпустите её на большую кнопку загрузки", "u_enpot": 'переключиться на простой интерфейс (может ускорить загрузку)', "u_depot": 'переключиться на модный интерфейс (может замедлить загрузку)', "u_gotpot": 'переключаюсь на простой интерфес для ускорения загрузки,\n\nможете переключиться обратно, если хотите!', "u_pott": "

          файлы:   {0} завершено,   {1} ошибок,   {2} загружаются,   {3} в очереди

          ", "u_ever": "это упрощённый загрузчик; up2k требует хотя бы
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'это упрощённый загрузчик; up2k лучше', "u_uput": 'увеличить скорость (пропуск подсчёта контрольных сумм)', "u_ewrite": 'у вас нет прав на запись в эту папку', "u_eread": 'у вас нет прав на чтение из этой папки', "u_enoi": 'поиск файлов выключен настройками сервера', "u_enoow": "перезапись здесь не работает; требуются права на удаление", "u_badf": 'Эти {0} из {1} файлов были пропущены, вероятно, из-за настроек доступа в файловой системе:\n\n', "u_blankf": 'Эти {0} из {1} файлов пустые; всё равно загрузить?\n\n', "u_applef": 'Эти {0} из {1} файлов, вероятно, нежелательны;\nНажмите OK/Enter, чтобы ПРОПУСТИТЬ их,\nНажмите Отмена/ESC, чтобы проигнорировать сообщение и ЗАГРУЗИТЬ их:\n\n', "u_just1": '\nВозможно, будет лучше, если вы выберете только один файл', "u_ff_many": "если вы используете Linux / MacOS / Android, тогда такое количество файлов может крашнуть Firefox!\nв таком случае попробуйте снова (или используйте Chrome).", "u_up_life": "Эта загрузка будет удалена с сервера\nчерез {0} после её завершения", "u_asku": 'загрузить эти {0} файлов в {1}', "u_unpt": "вы можете отменить / удалить эту загрузку с помощью 🧯 сверху слева", "u_bigtab": 'будет показано {0} файлов\n\nэто может крашнуть браузер, вы уверены?', "u_scan": 'Сканирую файлы...', "u_dirstuck": 'сканер папки завис при попытке получить доступ к следующим {0} файлам; будет пропущено:', "u_etadone": 'Готово ({0}, {1} файлов)', "u_etaprep": '(подготавливаю загрузку)', "u_hashdone": 'хеширование выполнено', "u_hashing": 'хеш', "u_hs": 'подготовка к хешированию...', "u_started": "файлы загружаются; подробнее в [🚀]", "u_dupdefer": "дубликат; будет обработан после всех остальных файлов", "u_actx": "нажмите на этот текст, чтобы предотвратить
          падение производительности при просмотре других вкладок / окон", "u_fixed": "Окей!  Исправлено 👍", "u_cuerr": "не удалось загрузить фрагмент {0} из {1};\nвероятно, не критично - продолжаю\n\nфайл: {2}", "u_cuerr2": "отказ в загрузке (фрагмент {0} из {1});\nпопытаюсь повторить позже\n\nфайл: {2}\n\nошибка ", "u_ehstmp": "попытаюсь повторить позже; подробнее снизу справа", "u_ehsfin": "отказ в запросе завершения загрузки; повторяю...", "u_ehssrch": "отказ в запросе поиска; повторяю...", "u_ehsinit": "отказ в запросе начала загрузки; повторяю...", "u_eneths": "ошибка подключения во время подготовки загрузки; повторяю...", "u_enethd": "ошибка подключения во время проверки существования целевого файла; повторяю...", "u_cbusy": "ожидаю возвращения доступа к серверу после ошибки подключения...", "u_ehsdf": "на сервере закончилось место!\n\nбуду пытаться повторить, на случай если\nместо будет освобождено", "u_emtleak1": "кажется, у вашего браузера может быть утечка памяти;\nпожалуйста,", "u_emtleak2": ' перейдите на https (рекомендовано) или ', "u_emtleak3": ' ', "u_emtleakc": 'попробуйте сделать так:\n
          • нажмите F5 для обновления страницы
          • затем отключите опцию  мп  в  ⚙️ настройках
          • и повторите попытку загрузки
          Она будет чуть медленнее, но что поделать.\nИзвините за неудобства!\n\nPS: в chrome v107 это исправили', "u_emtleakf": 'попробуйте сделать так:\n
          • нажмите F5 для обновления страницы
          • затем включите опцию 🥔 (простой интерфейс) во вкладке загрузок
          • и повторите попытку
          \nPS: firefox скоро должны починить в этом аспекте', "u_s404": "не найдено на сервере", "u_expl": "объяснить", "u_maxconn": "в большинстве браузеров это нельзя поднять выше 6, но firefox позволяет увеличить лимит с помощью connections-per-server в about:config", "u_tu": '

          ВНИМАНИЕ: активен режим турбо,  клиент может игнорировать незавершённые загрузки; подробнее при наведении на кнопку турбо

          ', "u_ts": '

          ВНИМАНИЕ: активен режим турбо,  результаты поиска могут быть некорректными; подробнее при наведении на кнопку турбо

          ', "u_turbo_c": "режим турбо отключён сервером", "u_turbo_g": "отключаю турбо, поскольку у вас нет прав\nна просмотр папок в этом хранилище", "u_life_cfg": 'автоудаление через мин (или часов)', "u_life_est": 'загрузка будет удалена ---', "u_life_max": 'эта папка требует\nавтоудаления файлов через {0}', "u_unp_ok": 'удаление разрешено для {0}', "u_unp_ng": 'удаление НЕ будет разрешено', "ue_ro": 'ваш доступ к данной папке - только чтение\n\n', "ue_nl": 'на данный момент вы не авторизованы', "ue_la": 'вы авторизованы как "{0}"', "ue_sr": 'сейчас вы в режиме поиска файлов\n\nперейдите в режим загрузки, нажав на лупу 🔎 (рядом с огромной кнопкой ИСКАТЬ), и попробуйте снова\n\nизвините', "ue_ta": 'попробуйте загрузить снова, теперь должно сработать', "ue_ab": "этот файл уже загружают в другую папку, та загрузка должна быть завершена перед тем, как загрузить этот же файл в другое место.\n\nВы можете отменить свою загрузку через 🧯 сверху слева", "ur_1uo": "OK: Файл успешно загружен", "ur_auo": "OK: Все {0} файлов успешно загружены", "ur_1so": "OK: Файл найден на сервере", "ur_aso": "OK: Все {0} файлов найдены на сервере", "ur_1un": "Загрузка не удалась, извините", "ur_aun": "Все {0} загрузок не удались, извините", "ur_1sn": "Файл НЕ был найден на сервере", "ur_asn": "Все {0} файлов НЕ было найдено на сервере", "ur_um": "Завершено;\n{0} успешно,\n{1} ошибок, извините", "ur_sm": "Завершено;\n{0} файлов найдено на сервере,\n{1} файлов НЕ найдено на сервере", "rc_opn": "открыть", //m "rc_ply": "воспроизвести", //m "rc_pla": "воспроизвести как аудио", //m "rc_txt": "открыть в просмотрщике файлов", //m "rc_md": "открыть в текстовом редакторе", //m "rc_dl": "скачать", //m "rc_zip": "скачать как архив", //m "rc_cpl": "копировать ссылку", //m "rc_del": "удалить", //m "rc_cut": "вырезать", //m "rc_cpy": "копировать", //m "rc_pst": "вставить", //m "rc_rnm": "переименовать", //m "rc_nfo": "новая папка", //m "rc_nfi": "новый файл", //m "rc_sal": "выбрать всё", //m "rc_sin": "инвертировать выделение", //m "rc_shf": "поделиться этой папкой", //m "rc_shs": "поделиться выделенным", //m "lang_set": "перезагрузить страницу, чтобы применить изменения?", "splash": { "a1": "обновить", "b1": "приветик, незнакомец   (вы не авторизованы)", "c1": "выйти", "d1": "трассировка стека", "d2": "показывает состояние всех активных потоков", "e1": "перезагрузить конфиг", "e2": "перезагрузить файлы конфига (аккаунты/хранилища/флаги),$Nи пересканировать все хранилища с флагом e2ds$N$Nвнимание: изменения глобальных настроек$Nтребуют полного перезапуска сервера", "f1": "вы можете видеть:", "g1": "вы можете загружать файлы в:", "cc1": "всякая всячина:", "h1": "отключить k304", "i1": "включить k304", "j1": "включённый k304 будет отключать вас при получении HTTP 304, что может помочь при работе с некоторыми глючными прокси (перестают загружаться страницы), но это также сделает работу клиента медленнее", "k1": "сбросить локальные настройки", "l1": "авторизуйтесь для других опций:", "ls3": "войти", //m "lu4": "имя пользователя", //m "lp4": "пароль", //m "lo3": "выйти из “{0}” везде", //m "lo2": "это завершит сеанс во всех браузерах", //m "m1": "с возвращением,", "n1": "404 не найдено  ┐( ´ -`)┌", "o1": 'или у вас нет доступа -- попробуйте авторизоваться или вернуться на главную', "p1": "403 доступ запрещён  ~┻━┻", "q1": 'авторизуйтесь или вернитесь на главную', "r1": "вернуться на главную", ".s1": "пересканировать", "t1": "действия", "u2": "время с последней записи на сервер$N( загрузка / переименование / ... )$N$N17d = 17 дней$N1h23 = 1 час 23 минут$N4m56 = 4 минут 56 секунд", "v1": "подключить", "v2": "использовать сервер как локальный диск", "w1": "перейти на https", "x1": "поменять пароль", "y1": "управление доступом", "z1": "разблокировать:", "ta1": "сначала введите свой новый пароль", "ta2": "повторите новый пароль:", "ta3": "опечатка; попробуйте снова", "nop": "ОШИБКА: Пароль не может быть пустым", //m "nou": "ОШИБКА: Имя пользователя и/или пароль не могут быть пустыми", //m "aa1": "входящие файлы:", "ab1": "отключить no304", "ac1": "включить no304", "ad1": "включённый no304 полностью отключит хеширование; используйте, если k304 не помог. Сильно увеличит объём трафика!", "ae1": "активные скачивания:", "af1": "показать недавние загрузки", "ag1": "показать известных IdP-пользователей", } }; ================================================ FILE: copyparty/web/tl/spa.js ================================================ // Las líneas que terminan con //m son traducciones automáticas no verificadas Ls.spa = { "tt": "Español", "cols": { "c": "acciones", "dur": "duración", "q": "calidad / bitrate", "Ac": "códec de audio", "Vc": "códec de vídeo", "Fmt": "formato / contenedor", "Ahash": "checksum de audio", "Vhash": "checksum de vídeo", "Res": "resolución", "T": "tipo de archivo", "aq": "calidad de audio / bitrate", "vq": "calidad de vídeo / bitrate", "pixfmt": "submuestreo / estructura de píxel", "resw": "resolución horizontal", "resh": "resolución vertical", "chs": "canales de audio", "hz": "frecuencia de muestreo", }, "hks": [ [ "varios", ["ESC", "cerrar varias cosas"], "gestor de archivos", ["G", "alternar vista de lista / cuadrícula"], ["T", "alternar miniaturas / iconos"], ["⇧ A/D", "tamaño de miniatura"], ["ctrl-K", "eliminar seleccionados"], ["ctrl-X", "cortar selección al portapapeles"], ["ctrl-C", "copiar selección al portapapeles"], ["ctrl-V", "pegar (mover/copiar) aquí"], ["Y", "descargar seleccionados"], ["F2", "renombrar seleccionados"], "selección en lista de archivos", ["space", "alternar selección de archivo"], ["↑/↓", "mover cursor de selección"], ["ctrl ↑/↓", "mover cursor y vista"], ["⇧ ↑/↓", "seleccionar anterior/siguiente archivo"], ["ctrl-A", "seleccionar todos los archivos / carpetas"] ], [ "navegación", ["B", "alternar breadcrumbs / panel de navegación"], ["I/K", "anterior/siguiente carpeta"], ["M", "carpeta de nivel superior (o contraer actual)"], ["V", "alternar carpetas / archivos en panel de navegación"], ["A/D", "tamaño del panel de navegación"] ], [ "reproductor de audio", ["J/L", "anterior/siguiente canción"], ["U/O", "saltar 10s atrás/adelante"], ["0..9", "saltar a 0%..90%"], ["P", "reproducir/pausar (también inicia)"], ["S", "seleccionar canción en reproducción"], ["Y", "descargar canción"] ], [ "visor de imágenes", ["J/L, ←/→", "anterior/siguiente imagen"], ["Home/End", "primera/última imagen"], ["F", "pantalla completa"], ["R", "rotar en sentido horario"], ["⇧ R", "rotar en sentido antihorario"], ["S", "seleccionar imagen"], ["Y", "descargar imagen"] ], [ "reproductor de vídeo", ["U/O", "saltar 10s atrás/adelante"], ["P/K/Space", "reproducir/pausar"], ["C", "continuar con el siguiente"], ["V", "bucle"], ["M", "silenciar"], ["[ y ]", "establecer intervalo de bucle"] ], [ "visor de texto", ["I/K", "anterior/siguiente archivo"], ["M", "cerrar archivo"], ["E", "editar archivo"], ["S", "seleccionar archivo (para cortar/copiar/renombrar)"], ["Y", "descargar archivo de texto"], //m ["⇧ J", "embellecer json"], //m ] ], "m_ok": "Aceptar", "m_ng": "Cancelar", "enable": "Activar", "danger": "PELIGRO", "clipped": "copiado al portapapeles", "ht_s1": "segundo", "ht_s2": "segundos", "ht_m1": "minuto", "ht_m2": "minutos", "ht_h1": "hora", "ht_h2": "horas", "ht_d1": "día", "ht_d2": "días", "ht_and": " y ", "goh": "panel de control", "gop": 'hermano anterior">anterior', "gou": 'carpeta de nivel superior">subir', "gon": 'siguiente carpeta">siguiente', "logout": "Cerrar sesión ", "login": "Iniciar sesión", //m "access": " acceso", "ot_close": "cerrar submenú", "ot_search": "`buscar archivos por atributos, ruta / nombre, etiquetas de música, o cualquier combinación$N$N`foo bar` = debe contener «foo» y «bar»,$N`foo -bar` = debe contener «foo» pero no «bar»,$N`^yana .opus$` = empieza con «yana» y es un archivo «opus»$N`"try unite"` = contiene exactamente «try unite»$N$Nel formato de fecha es iso-8601, como$N`2009-12-31` o `2020-09-12 23:30:00`", "ot_unpost": "dessubir: elimina tus subidas recientes, o aborta las inacabadas", "ot_bup": "bup: uploader básico, soporta hasta netscape 4.0", "ot_mkdir": "mkdir: crear un nuevo directorio", "ot_md": "new-file: crear un nuevo archivo de texto", //m "ot_msg": "msg: enviar un mensaje al registro del servidor", "ot_mp": "opciones del reproductor multimedia", "ot_cfg": "opciones de configuración", "ot_u2i": "up2k: subir archivos (si tienes acceso de escritura) o cambiar a modo de búsqueda para ver si existen en el servidor$N$Nlas subidas se pueden reanudar, usan múltiples hilos y conservan la fecha de los archivos, pero consume más CPU que [🎈]  (el uploader básico)

          ¡Durante las subidas, este icono se convierte en un indicador de progreso!", "ot_u2w": "up2k: subir archivos con soporte para reanudación (cierra tu navegador y arrastra los mismos archivos más tarde)$N$NMultihilo y conserva las fechas de los archivos, pero usa más CPU que [🎈]  (el uploader básico)

          ¡Durante las subidas, este icono se convierte en un indicador de progreso!", "ot_noie": "Por favor, usa Chrome / Firefox / Edge", "ab_mkdir": "crear directorio", "ab_mkdoc": "nuevo archivo de texto", //m "ab_msg": "enviar msg al registro del servidor", "ay_path": "saltar a carpetas", "ay_files": "saltar a archivos", "wt_ren": "renombrar elementos seleccionados$NAtajo: F2", "wt_del": "eliminar elementos seleccionados$NAtajo: ctrl-K", "wt_cut": "cortar elementos seleccionados <small>(luego pegar en otro lugar)</small>$NAtajo: ctrl-X", "wt_cpy": "copiar elementos seleccionados al portapapeles$N(para pegarlos en otro lugar)$NAtajo: ctrl-C", "wt_pst": "pegar una selección previamente cortada / copiada$NAtajo: ctrl-V", "wt_selall": "seleccionar todos los archivos$NAtajo: ctrl-A (con un archivo con foco)", "wt_selinv": "invertir selección", "wt_zip1": "descargar esta carpeta como un archivo comprimido", "wt_selzip": "descargar selección como archivo comprimido", "wt_seldl": "descargar selección como archivos separados$NAtajo: Y", "wt_npirc": "copiar información de pista en formato IRC", "wt_nptxt": "copiar información de pista en texto plano", "wt_m3ua": "añadir a lista m3u (haz clic en 📻copiar después)", "wt_m3uc": "copiar lista m3u al portapapeles", "wt_grid": "alternar vista de cuadrícula / lista$NAtajo: G", "wt_prev": "pista anterior$NAtajo: J", "wt_play": "reproducir / pausar$NAtajo: P", "wt_next": "siguiente pista$NAtajo: L", "ul_par": "subidas paralelas:", "ut_rand": "aleatorizar nombres de archivo", "ut_u2ts": 'copiar la fecha de última modificación$Nde tu sistema de archivos al servidor">📅', "ut_ow": "sobrescribir archivos existentes en el servidor?$N🛡️: nunca (generará un nuevo nombre de archivo en su lugar)$N🕒: sobrescribir si el archivo del servidor es más antiguo que el tuyo$N♻️: siempre sobrescribir si los archivos son diferentes$N⏭️: omitir incondicionalmente todos los archivos existentes", //m "ut_mt": "continuar generando hashes de otros archivos mientras se sube$N$Nquizás desactivar si tu CPU o HDD es un cuello de botella", "ut_ask": 'pedir confirmación antes de iniciar la subida">💭', "ut_pot": "mejorar la velocidad de subida en dispositivos lentos$Nsimplificando la interfaz de usuario", "ut_srch": "no subir, en su lugar comprobar si los archivos ya $N existen en el servidor (escaneará todas las carpetas que puedas leer)", "ut_par": "pausar subidas poniéndolo a 0$N$Naumentar si tu conexión es lenta / de alta latencia$N$Nmantener en 1 en LAN o si el HDD del servidor es un cuello de botella", "ul_btn": "arrastra archivos / carpetas
          aquí (o haz clic)", "ul_btnu": "S U B I R", "ul_btns": "B U S C A R", "ul_hash": "hash", "ul_send": "envio", "ul_done": "hecho", "ul_idle1": "aún no hay subidas en cola", "ut_etah": "velocidad media de <em>hashing</em>, y tiempo estimado para finalizar", "ut_etau": "velocidad media de <em>subida</em> y tiempo estimado para finalizar", "ut_etat": "velocidad media <em>total</em> y tiempo estimado para finalizar", "uct_ok": "completado con éxito", "uct_ng": "fallido: error / rechazado / no encontrado", "uct_done": "éxitos y fallos combinados", "uct_bz": "generando hash o subiendo", "uct_q": "inactivo, pendiente", "utl_name": "nombre de archivo", "utl_ulist": "lista", "utl_ucopy": "copiar", "utl_links": "enlaces", "utl_stat": "estado", "utl_prog": "progreso", "utl_404": "404", "utl_err": "ERROR", "utl_oserr": "Error-SO", "utl_found": "encontrado", "utl_defer": "posponer", "utl_yolo": "YOLO", "utl_done": "hecho", "ul_flagblk": "los archivos se añadieron a la cola
          sin embargo, hay un up2k ocupado en otra pestaña del navegador,
          esperando a que termine primero", "ul_btnlk": "la configuración del servidor ha bloqueado esta opción en este estado", "udt_up": "Subir", "udt_srch": "Buscar", "udt_drop": "suéltalo aquí", "u_nav_m": "
          vale, ¿qué tienes?
          Intro = Archivos (uno o más)\nESC = Una carpeta (incluyendo subcarpetas)", "u_nav_b": "ArchivosUna carpeta", "cl_opts": "opciones", "cl_hfsz": "tamaño del archivo", //m "cl_themes": "tema", "cl_langs": "idioma", "cl_ziptype": "descarga de carpeta", "cl_uopts": "opciones up2k", "cl_favico": "favicon", "cl_bigdir": "directorios grandes", "cl_hsort": "#ordenar", "cl_keytype": "notación musical", "cl_hiddenc": "columnas ocultas", "cl_hidec": "ocultar", "cl_reset": "restablecer", "cl_hpick": "toca en las cabeceras de columna para ocultarlas en la tabla de abajo", "cl_hcancel": "ocultación de columna cancelada", "cl_rcm": "menú contextual", //m "ct_grid": '田 cuadrícula', "ct_ttips": '◔ ◡ ◔">ℹ️ tooltips', "ct_thumb": 'en vista de cuadrícula, alternar iconos o miniaturas$NAtajo: T">🖼️ miniaturas', "ct_csel": 'usa CTRL y SHIFT para seleccionar archivos en la vista de cuadrícula">sel', "ct_dsel": 'usa la selección por arrastre en la vista de cuadrícula">arrastrar', //m "ct_dl": 'forzar descarga (no mostrar en línea) al hacer clic en un archivo">dl', //m "ct_ihop": 'al cerrar el visor de imágenes, desplazarse hasta el último archivo visto">g⮯', "ct_dots": 'mostrar archivos ocultos (si el servidor lo permite)">archivos ocultos', "ct_qdel": 'al eliminar archivos, pedir confirmación solo una vez">elim. rápida', "ct_dir1st": 'ordenar carpetas antes que archivos">📁 primero', "ct_nsort": 'orden natural (para nombres de archivo con dígitos iniciales)">ord. natural', "ct_utc": 'use UTC para todas las horas">UTC', //m "ct_readme": 'mostrar README.md en los listados de carpetas">📜 léeme', "ct_idxh": 'mostrar index.html en lugar del listado de carpetas">htm', "ct_sbars": 'mostrar barra lateral">⟊', "cut_umod": 'si un archivo ya existe en el servidor, actualiza la fecha de última modificación del servidor para que coincida con tu archivo local (requiere permisos de escritura+eliminación)">re📅', "cut_turbo": 'el botón yolo, probablemente NO quieras activarlo:$N$Núsalo si estabas subiendo una gran cantidad de archivos y tuviste que reiniciar por alguna razón, y quieres continuar la subida lo antes posible$N$Nesto reemplaza la comprobación de hash por un simple "¿tiene este el mismo tamaño de archivo en el servidor?" así que si el contenido del archivo es diferente, NO se subirá$N$Ndeberías desactivar esto cuando la subida termine, y luego "subir" los mismos archivos de nuevo para que el cliente los verifique">turbo', "cut_datechk": 'no tiene efecto a menos que el botón turbo esté activado$N$Nreduce el factor yolo en una pequeña cantidad; comprueba si las fechas de los archivos en el servidor coinciden con las tuyas$N$Nteóricamente debería detectar la mayoría de las subidas inacabadas / corruptas, pero no es un sustituto de hacer una pasada de verificación con el turbo desactivado después">verif. fecha', "cut_u2sz": "tamaño (en MiB) de cada trozo de subida; los valores grandes vuelan mejor a través del atlántico. Prueba valores bajos en conexiones muy poco fiables", "cut_flag": "asegura que solo una pestaña esté subiendo a la vez $N -- otras pestañas también deben tener esto activado $N -- solo afecta a pestañas en el mismo dominio", "cut_az": "subir archivos en orden alfabético, en lugar de los más pequeños primero$N$Nel orden alfabético puede facilitar la detección visual de si algo salió mal en el servidor, pero hace la subida ligeramente más lenta en fibra / LAN", "cut_nag": "notificación del SO cuando la subida se complete$N(solo si el navegador o la pestaña no están activos)", "cut_sfx": "alerta sonora cuando la subida se complete$N(solo si el navegador o la pestaña no están activos)", "cut_mt": 'usar multithreading para acelerar el hashing de archivos$N$Nesto usa web-workers y requiere$Nmás RAM (hasta 512 MiB extra)$N$Nhace https un 30% más rápido, http 4.5x más rápido">mt', "cut_wasm": 'usar wasm en lugar del hasher incorporado del navegador; mejora la velocidad en navegadores basados en chrome pero aumenta la carga de la CPU, y muchas versiones antiguas de chrome tienen errores que hacen que el navegador consuma toda la RAM y se bloquee si esto está activado">wasm', "cft_text": "texto del favicon (dejar en blanco y refrescar para desactivar)", "cft_fg": "color de primer plano", "cft_bg": "color de fondo", "cdt_lim": "número máximo de archivos a mostrar en una carpeta", "cdt_ask": "al llegar al final,$Nen lugar de cargar más archivos,$Npreguntar qué hacer", "cdt_hsort": "`cuántas reglas de ordenación (`,sorthref`) incluir en las URLs de medios. Ponerlo a 0 también ignorará las reglas de ordenación incluidas en los enlaces de medios al hacer clic en ellos", "cdt_ren": "habilitar menú contextual personalizado, el menú normal sigue siendo accesible con shift + clic derecho\">activar", //m "cdt_rdb": "mostrar el menú normal de clic derecho cuando el personalizado ya está abierto y se vuelve a hacer clic\">x2", //m "tt_entree": "mostrar panel de navegación (barra lateral con árbol de directorios)$NAtajo: B", "tt_detree": "mostrar breadcrumbs$NAtajo: B", "tt_visdir": "desplazarse a la carpeta seleccionada", "tt_ftree": "alternar árbol de carpetas / archivos de texto$NAtajo: V", "tt_pdock": "mostrar carpetas de niveles superiores en un panel acoplado en la parte superior", "tt_dynt": "crecimiento automático a medida que el árbol se expande", "tt_wrap": "ajuste de línea", "tt_hover": "revelar líneas que se desbordan al pasar el ratón$N( rompe el desplazamiento a menos que el $N  cursor esté en el margen izquierdo )", "ml_pmode": "al final de la carpeta...", "ml_btns": "acciones", "ml_tcode": "transcodificar", "ml_tcode2": "transcodificar a", "ml_tint": "tinte", "ml_eq": "ecualizador de audio", "ml_drc": "compresor de rango dinámico", "ml_ss": "saltar silencios", //m "mt_loop": 'poner en bucle/repetir una canción">🔁', "mt_one": 'parar después de una canción">1️⃣', "mt_shuf": 'reproducir aleatoriamente las canciones en cada carpeta">🔀', "mt_aplay": 'reproducir automaticamente si hay un ID de canción en el enlace en el que hiciste clic para acceder al servidor$N$Ndesactivar esto también evitará que la URL de la página se actualice con IDs de canción al reproducir música, para prevenir la reproducción automática si se pierden estos ajustes pero la URL permanece">a▶', "mt_preload": 'empezar a cargar la siguiente canción cerca del final para una reproducción sin pausas">precarga', "mt_prescan": 'ir a la siguiente carpeta antes de que la última canción$Ntermine, manteniendo contento al navegador$Npara que no detenga la reproducción">nav', "mt_fullpre": 'intentar precargar la canción entera;$N✅ activar en conexiones inestables,$N❌ desactivar probablemente en conexiones lentas">completa', "mt_fau": 'en teléfonos, evitar que la música se detenga si la siguiente canción no se precarga lo suficientemente rápido (puede causar fallos en la visualización de etiquetas)">☕️', "mt_waves": 'barra de búsqueda con forma de onda:$Nmostrar la amplitud del audio en la barra de progreso">~s', "mt_npclip": 'mostrar botones para copiar al portapapeles la canción actual">/np', "mt_m3u_c": 'mostrar botones para copiar al portapapeles las$Ncanciones seleccionadas como entradas de lista m3u8">📻', "mt_octl": 'integración con SO (teclas multimedia / OSD)">ctl-so', "mt_oseek": 'permitir buscar a través de la integración con el SO$N$Nnota: en algunos dispositivos (iPhones),$Nesto reemplaza el botón de siguiente canción">búsqueda', "mt_oscv": 'mostrar carátula del álbum en OSD">arte', "mt_follow": 'mantener la pista en reproducción visible en pantalla">🎯', "mt_compact": 'controles compactos">⟎', "mt_uncache": 'limpiar caché  (prueba esto si tu navegador guardó en caché$Nuna copia rota de una canción que se niega a reproducir)">limpiar caché', "mt_mloop": 'repetir la carpeta actual">🔁 bucle', "mt_mnext": 'cargar la siguiente carpeta y continuar">📂 sig', "mt_mstop": 'detener reproducción">⏸ parar', "mt_cflac": 'convertir flac / wav a {0}">flac', "mt_caac": 'convertir aac / m4a a {0}">aac', "mt_coth": 'convertir todos los demás (no mp3) a {0}">oth', "mt_c2opus": 'la mejor opción para ordenadores, portátiles, android">opus', "mt_c2owa": 'opus-weba, para iOS 17.5 y superior">owa', "mt_c2caf": 'opus-caf, para iOS 11 a 17">caf', "mt_c2mp3": 'usar en dispositivos muy antiguos">mp3', "mt_c2flac": "la mejor calidad de sonido,$Npero descargas muy grandes\">flac", //m "mt_c2wav": "reproducción sin comprimir (aún más grande)\">wav", //m "mt_c2ok": "bien, buena elección", "mt_c2nd": "ese no es el formato de salida recomendado para tu dispositivo, pero está bien", "mt_c2ng": "tu dispositivo no parece soportar este formato de salida, pero intentémoslo de todas formas", "mt_xowa": "hay errores en iOS que impiden la reproducción en segundo plano con este formato; por favor, usa caf o mp3 en su lugar", "mt_tint": "nivel de fondo (0-100) en la barra de búsqueda$Npara hacer el buffering menos molesto", "mt_eq": "`activa el ecualizador y el control de ganancia;$N$Nganancia `0` = volumen estándar 100% (sin modificar)$N$Nancho `1  ` = estéreo estándar (sin modificar)$Nancho `0.5` = 50% de crossfeed izq-der$Nancho `0  ` = mono$N$Nganancia `-0.8` y ancho `10` = eliminación de voz :^)$N$Nactivar el ecualizador hace que los álbumes sin pausas sean completamente sin pausas, así que déjalo activado con todos los valores a cero (excepto ancho = 1) si eso te importa", "mt_drc": "activa el compresor de rango dinámico (aplanador de volumen / brickwaller); también activará el EQ para equilibrar el espagueti, así que pon todos los campos de EQ excepto 'ancho' a 0 si no lo quieres$N$Nbaja el volumen del audio por encima de THRESHOLD dB; por cada RATIO dB pasado THRESHOLD hay 1 dB de salida, así que los valores por defecto de umbral -24 y ratio 12 significan que nunca debería sonar más fuerte de -22 dB y es seguro aumentar la ganancia del ecualizador a 0.8, o incluso 1.8 con ATK 0 y un RLS enorme como 90 (solo funciona en firefox; RLS es máx. 1 en otros navegadores)$N$N(ver wikipedia, lo explican mucho mejor)", "mt_ss": "`activa salto de silencio; multiplica la velocidad por `av` cerca del inicio/fin cuando el volumen está bajo `vol` y la posición está en los primeros `ini`% o últimos `fin`%", //m "mt_ssvt": "umbral de volumen (0-255)\">vol", //m "mt_ssts": "umbral activo (% pista, inicio)\">ini", //m "mt_sste": "umbral activo (% pista, fin)\">fin", //m "mt_sssm": "multiplicador de velocidad de reproducción\">av", //m "mb_play": "reproducir", "mm_hashplay": "¿reproducir este archivo de audio?", "mm_m3u": "pulsa Intro/Aceptar para Reproducir\npulsa ESC/Cancelar para Editar", "mp_breq": "se necesita firefox 82+ o chrome 73+ o iOS 15+", "mm_bload": "cargando...", "mm_bconv": "convirtiendo a {0}, por favor espera...", "mm_opusen": "tu navegador no puede reproducir archivos aac / m4a;\nse ha activado la transcodificación a opus", "mm_playerr": "fallo de reproducción: ", "mm_eabrt": "El intento de reproducción fue cancelado", "mm_enet": "Tu conexión a internet es inestable", "mm_edec": "¿Este archivo está supuestamente corrupto?", "mm_esupp": "Tu navegador no entiende este formato de audio", "mm_eunk": "Error desconocido", "mm_e404": "No se pudo reproducir el audio; error 404: Archivo no encontrado.", "mm_e403": "No se pudo reproducir el audio; error 403: Acceso denegado.\n\nIntenta pulsar F5 para recargar, quizás se cerró tu sesión", "mm_e415": "No se pudo reproducir el audio; error 415: Falló la conversión del archivo; revisa los registros del servidor.", //m "mm_e500": "No se pudo reproducir el audio; error 500: Revisa los registros del servidor.", "mm_e5xx": "No se pudo reproducir el audio; error del servidor ", "mm_nof": "no se encuentran más archivos de audio cerca", "mm_prescan": "Buscando música para reproducir a continuación...", "mm_scank": "Encontrada la siguiente canción:", "mm_uncache": "caché limpiada; todas las canciones se volverán a descargar en la próxima reproducción", "mm_hnf": "esa canción ya no existe", "im_hnf": "esa imagen ya no existe", "f_empty": "esta carpeta está vacía", "f_chide": "esto ocultará la columna «{0}»\n\npuedes volver a mostrar las columnas en la pestaña de configuración", "f_bigtxt": "este archivo pesa {0} MiB -- ¿realmente verlo como texto?", "f_bigtxt2": "¿ver solo el final del archivo en su lugar? esto también activará el seguimiento, mostrando las líneas de texto recién añadidas en tiempo real", "fbd_more": '
          mostrando {0} de {1} archivos; mostrar {2} o mostrar todos
          ', "fbd_all": '
          mostrando {0} de {1} archivos; mostrar todos
          ', "f_anota": "solo {0} de los {1} elementos fueron seleccionados;\npara seleccionar la carpeta completa, primero desplázate hasta el final", "f_dls": "los enlaces a archivos en la carpeta actual se han\nconvertido en enlaces de descarga", "f_dl_nd": 'omitiendo carpeta (use la descarga zip/tar en su lugar):\n', //m "f_partial": "Para descargar de forma segura un archivo que se está subiendo actualmente, por favor haz clic en el archivo con el mismo nombre, pero sin la extensión .PARTIAL. Por favor, pulsa CANCELAR o Escape para hacer esto.\n\nPulsar ACEPTAR o Intro ignorará esta advertencia y continuará descargando el archivo temporal .PARTIAL, lo que casi con toda seguridad te dará datos corruptos.", "ft_paste": "pegar {0} elementos$NAtajo: ctrl-V", "fr_eperm": "no se puede renombrar:\nno tienes permiso de “mover” en esta carpeta", "fd_eperm": "no se puede eliminar:\nno tienes permiso de “eliminar” en esta carpeta", "fc_eperm": "no se puede cortar:\nno tienes permiso de “mover” en esta carpeta", "fp_eperm": "no se puede pegar:\nno tienes permiso de “escribir” en esta carpeta", "fr_emore": "selecciona al menos un elemento para renombrar", "fd_emore": "selecciona al menos un elemento para eliminar", "fc_emore": "selecciona al menos un elemento para cortar", "fcp_emore": "selecciona al menos un elemento para copiar al portapapeles", "fs_sc": "compartir la carpeta en la que estás", "fs_ss": "compartir los archivos seleccionados", "fs_just1d": "no puedes seleccionar más de una carpeta,\no mezclar archivos y carpetas en una selección", "fs_abrt": "❌ abortar", "fs_rand": "🎲 nombre aleatorio", "fs_go": "✅ crear enlace", "fs_name": "nombre", "fs_src": "origen", "fs_pwd": "contraseña", "fs_exp": "caducidad", "fs_tmin": "minutos", "fs_thrs": "horas", "fs_tdays": "días", "fs_never": "eterno", "fs_pname": "nombre opcional del enlace; será aleatorio si se deja en blanco", "fs_tsrc": "el archivo o carpeta a compartir", "fs_ppwd": "contraseña opcional", "fs_w8": "creando enlace...", "fs_ok": "pulsa Intro/Aceptar para Copiar al Portapapeles\npulsa ESC/Cancelar para Cerrar", "frt_dec": "puede arreglar algunos casos de nombres de archivo rotos\">url-decode", "frt_rst": "restaurar los nombres de archivo modificados a los originales\">↺ restablecer", "frt_abrt": "abortar y cerrar esta ventana\">❌ cancelar", "frb_apply": "APLICAR RENOMBRADO", "fr_adv": "renombrado por lotes / metadatos / patrones\">avanzado", "fr_case": "regex sensible a mayúsculas\">mayús", "fr_win": "nombres seguros para windows; reemplaza <>:"\\|?* con caracteres japoneses de ancho completo\">win", "fr_slash": "reemplaza / con un carácter que no cree nuevas carpetas\">sin /", "fr_re": "`patrón de búsqueda regex para aplicar a los nombres de archivo originales; los grupos de captura se pueden referenciar en el campo de formato de abajo como `(1)` y `(2)` y así sucesivamente", "fr_fmt": "`inspirado en foobar2000:$N`(title)` se reemplaza por el título de la canción,$N`[(artist) - ](title)` omite la parte [entre corchetes] si el artista está en blanco$N`$lpad((tn),2,0)` rellena el número de pista a 2 dígitos", "fr_pdel": "eliminar", "fr_pnew": "guardar como", "fr_pname": "proporciona un nombre para tu nuevo preajuste", "fr_aborted": "abortado", "fr_lold": "nombre antiguo", "fr_lnew": "nombre nuevo", "fr_tags": "etiquetas para los archivos seleccionados (solo lectura, como referencia):", "fr_busy": "renombrando {0} elementos...\n\n{1}", "fr_efail": "fallo al renombrar:\n", "fr_nchg": "{0} de los nuevos nombres fueron alterados debido a win y/o sin /\n\n¿Aceptar para continuar con estos nuevos nombres alterados?", "fd_ok": "eliminación correcta", "fd_err": "fallo al eliminar:\n", "fd_none": "no se eliminó nada; quizás bloqueado por la configuración del servidor (xbd)?", "fd_busy": "eliminando {0} elementos...\n\n{1}", "fd_warn1": "¿ELIMINAR estos {0} elementos?", "fd_warn2": "¡Última oportunidad! No se puede deshacer. ¿Eliminar?", "fc_ok": "cortados {0} elementos", "fc_warn": "cortados {0} elementos\n\npero: solo esta pestaña del navegador puede pegarlos\n(dado que la selección es absolutamente masiva)", "fcc_ok": "copiados {0} elementos al portapapeles", "fcc_warn": "copiados {0} elementos al portapapeles\n\npero: solo esta pestaña del navegador puede pegarlos\n(dado que la selección es absolutamente masiva)", "fp_apply": "usar estos nombres", "fp_skip": "omitir conflictos", //m "fp_ecut": "primero corta o copia algunos archivos / carpetas para pegar / mover\n\nnota: puedes cortar / pegar entre diferentes pestañas del navegador", "fp_ename": "{0} elementos no se pueden mover aquí porque los nombres ya existen. Dales nuevos nombres abajo para continuar, o deja el nombre en blanco (\"omitir conflictos\") para omitirlos:", //m "fcp_ename": "{0} elementos no se pueden copiar aquí porque los nombres ya existen. Dales nuevos nombres abajo para continuar, o deja el nombre en blanco (\"omitir conflictos\") para omitirlos:", //m "fp_emore": "todavía quedan algunas colisiones de nombres por resolver", "fp_ok": "movimiento correcto", "fcp_ok": "copia correcta", "fp_busy": "moviendo {0} elementos...\n\n{1}", "fcp_busy": "copiando {0} elementos...\n\n{1}", "fp_abrt": "cancelando...", //m "fp_err": "fallo al mover:\n", "fcp_err": "fallo al copiar:\n", "fp_confirm": "¿mover estos {0} elementos aquí?", "fcp_confirm": "¿copiar estos {0} elementos aquí?", "fp_etab": "fallo al leer el portapapeles de otra pestaña del navegador", "fp_name": "subiendo un archivo desde tu dispositivo. Dale un nombre:", "fp_both_m": "
          elige qué pegar
          Intro = Mover {0} archivos desde «{1}»\nESC = Subir {2} archivos desde tu dispositivo", "fcp_both_m": "
          elige qué pegar
          Intro = Copiar {0} archivos desde «{1}»\nESC = Subir {2} archivos desde tu dispositivo", "fp_both_b": "MoverSubir", "fcp_both_b": "CopiarSubir", "mk_noname": "escribe un nombre en el campo de texto de la izquierda antes de hacer eso :p", "nmd_i1": "también puedes añadir la extensión que quieras, por ejemplo .md", //m "nmd_i2": "solo puedes crear archivos .{0} porque no tienes permiso para borrar", //m "tv_load": "Cargando documento de texto:\n\n{0}\n\n{1}% ({2} de {3} MiB cargados)", "tv_xe1": "no se pudo cargar el archivo de texto:\n\nerror ", "tv_xe2": "404, archivo no encontrado", "tv_lst": "lista de archivos de texto en", "tvt_close": "volver a la vista de carpetas$NAtajo: M (o Esc)\">❌ cerrar", "tvt_dl": "descargar este archivo$NAtajo: Y\">💾 descargar", "tvt_prev": "mostrar documento anterior$NAtajo: i\">⬆ ant", "tvt_next": "mostrar siguiente documento$NAtajo: K\">⬇ sig", "tvt_sel": "seleccionar archivo   ( para cortar / copiar / eliminar / ... )$NAtajo: S\">sel", "tvt_j": "embellecer json$NAtajo: shift-J\">j", //m "tvt_edit": "abrir archivo en editor de texto$NAtajo: E\">✏️ editar", "tvt_tail": "monitorizar cambios en el archivo; mostrar nuevas líneas en tiempo real\">📡 seguir", "tvt_wrap": "ajuste de línea\">↵", "tvt_atail": "bloquear el desplazamiento al final de la página\">⚓", "tvt_ctail": "decodificar colores de terminal (códigos de escape ansi)\">🌈", "tvt_ntail": "límite de historial (cuántos bytes de texto mantener cargados)", "m3u_add1": "canción añadida a la lista m3u", "m3u_addn": "{0} canciones añadidas a la lista m3u", "m3u_clip": "lista m3u copiada al portapapeles\n\ndebes crear un nuevo archivo de texto llamado algo.m3u y pegar la lista en ese documento; esto lo hará reproducible", "gt_vau": "no mostrar vídeos, solo reproducir el audio\">🎧", "gt_msel": "activar selección de archivos; ctrl-clic en un archivo para anular$N$N<em>cuando está activo: doble clic en un archivo / carpeta para abrirlo</em>$N$NAtajo: S\">multiselección", "gt_crop": "recortar miniaturas\">recortar", "gt_3x": "miniaturas de alta resolución\">3x", "gt_zoom": "zoom", "gt_chop": "recortar", "gt_sort": "ordenar por", "gt_name": "nombre", "gt_sz": "tamaño", "gt_ts": "fecha", "gt_ext": "tipo", "gt_c1": "truncar más los nombres de archivo (mostrar menos)", "gt_c2": "truncar menos los nombres de archivo (mostrar más)", "sm_w8": "buscando...", "sm_prev": "los resultados de búsqueda a continuación son de una consulta anterior:\n ", "sl_close": "cerrar resultados de búsqueda", "sl_hits": "mostrando {0} resultados", "sl_moar": "cargar más", "s_sz": "tamaño", "s_dt": "fecha", "s_rd": "ruta", "s_fn": "nombre", "s_ta": "etiquetas", "s_ua": "subido@", "s_ad": "avanzado", "s_s1": "MiB mínimo", "s_s2": "MiB máximo", "s_d1": "mín. iso8601", "s_d2": "máx. iso8601", "s_u1": "subido después de", "s_u2": "y/o antes de", "s_r1": "la ruta contiene   (separado por espacios)", "s_f1": "el nombre contiene   (negar con -no)", "s_t1": "las etiquetas contienen   (^=inicio, fin=$)", "s_a1": "propiedades de metadatos específicas", "md_eshow": "no se puede renderizar ", "md_off": "[📜léeme] desactivado en [⚙️] -- documento oculto", "badreply": "Fallo al procesar la respuesta del servidor", "xhr403": "403: Acceso denegado\n\nintenta pulsar F5, quizás se cerró tu sesión", "xhr0": "desconocido (probablemente se perdió la conexión con el servidor, o el servidor está desconectado)", "cf_ok": "perdón por eso -- la protección DD" + wah + "oS se activó\n\nlas cosas deberían reanudarse en unos 30 segundos\n\nsi no pasa nada, pulsa F5 para recargar la página", "tl_xe1": "no se pudieron listar las subcarpetas:\n\nerror ", "tl_xe2": "404: Carpeta no encontrada", "fl_xe1": "no se pudieron listar los archivos en la carpeta:\n\nerror ", "fl_xe2": "404: Carpeta no encontrada", "fd_xe1": "no se pudo crear la subcarpeta:\n\nerror ", "fd_xe2": "404: Carpeta de nivel superior no encontrada", "fsm_xe1": "no se pudo enviar el mensaje:\n\nerror ", "fsm_xe2": "404: Carpeta de nivel superior no encontrada", "fu_xe1": "fallo al cargar la lista de deshacer del servidor:\n\nerror ", "fu_xe2": "404: ¿Archivo no encontrado?", "fz_tar": "archivo gnu-tar sin comprimir (linux / mac)", "fz_pax": "tar formato pax sin comprimir (más lento)", "fz_targz": "gnu-tar con compresión gzip nivel 3$N$Nesto suele ser muy lento, así que$Nusa tar sin comprimir en su lugar", "fz_tarxz": "gnu-tar con compresión xz nivel 1$N$Nesto suele ser muy lento, así que$Nusa tar sin comprimir en su lugar", "fz_zip8": "zip con nombres de archivo utf8 (puede dar problemas en windows 7 y anteriores)", "fz_zipd": "zip con nombres de archivo cp437 tradicionales, para software muy antiguo", "fz_zipc": "cp437 con crc32 calculado tempranamente,$Npara MS-DOS PKZIP v2.04g (octubre 1993)$N(tarda más en procesar antes de que la descarga pueda empezar)", "un_m1": "puedes eliminar tus subidas recientes (o abortar las inacabadas) a continuación", "un_upd": "actualizar", "un_m4": "o compartir los archivos visibles a continuación:", "un_ulist": "mostrar", "un_ucopy": "copiar", "un_flt": "filtro opcional:  la URL debe contener", "un_fclr": "limpiar filtro", "un_derr": "fallo al deshacer-eliminar:\n", "un_f5": "algo se rompió, por favor intenta actualizar o pulsa F5", "un_uf5": "lo siento pero tienes que refrescar la página (por ejemplo pulsando F5 o CTRL-R) antes de que esta subida pueda ser abortada", "un_nou": "aviso: servidor demasiado ocupado para mostrar subidas inacabadas; haz clic en el enlace \"actualizar\" en un momento", "un_noc": "aviso: la opción de deshacer subidas completadas no está activada/permitida en la configuración del servidor", "un_max": "mostrando los primeros 2000 archivos (usa el filtro)", "un_avail": "{0} subidas recientes se pueden eliminar
          {1} inacabadas se pueden abortar", "un_m2": "ordenado por tiempo de subida; más recientes primero:", "un_no1": "¡pues no! ninguna subida es suficientemente reciente", "un_no2": "¡pues no! ninguna subida que coincida con ese filtro es suficientemente reciente", "un_next": "eliminar los siguientes {0} archivos a continuación", "un_abrt": "abortar", "un_del": "eliminar", "un_m3": "cargando tus subidas recientes...", "un_busy": "eliminando {0} archivos...", "un_clip": "{0} enlaces copiados al portapapeles", "u_https1": "deberías", "u_https2": "cambiar a https", "u_https3": "para un mejor rendimiento", "u_ancient": "tu navegador es impresionantemente antiguo -- quizás deberías usar bup en su lugar", "u_nowork": "se necesita firefox 53+ o chrome 57+ o iOS 11+", "tail_2old": "se necesita firefox 105+ o chrome 71+ o iOS 14.5+", "u_nodrop": "tu navegador es demasiado antiguo para subir arrastrando y soltando", "u_notdir": "¡eso no es una carpeta!\n\ntu navegador es demasiado antiguo,\npor favor intenta arrastrar y soltar en su lugar", "u_uri": "para arrastrar y soltar imágenes desde otras ventanas del navegador,\npor favor suéltalas sobre el gran botón de subida", "u_enpot": "cambiar a UI ligera (puede mejorar la velocidad de subida)", "u_depot": "cambiar a UI elegante (puede reducir la velocidad de subida)", "u_gotpot": "cambiando a la UI ligera para mejorar la velocidad de subida,\n\n¡siéntete libre de no estar de acuerdo y volver a cambiar!", "u_pott": "

          archivos:   {0} finalizados,   {1} fallidos,   {2} ocupados,   {3} en cola

          ", "u_ever": "este es el uploader básico; up2k necesita al menos
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": "este es el uploader básico; up2k es mejor", "u_uput": "optimizar para velocidad (omitir checksum)", "u_ewrite": "no tienes acceso de escritura a esta carpeta", "u_eread": "no tienes acceso de lectura a esta carpeta", "u_enoi": "la búsqueda de archivos no está activada en la configuración del servidor", "u_enoow": "sobrescribir no funcionará aquí; se necesita permiso de eliminación", "u_badf": "Estos {0} archivos (de un total de {1}) se omitieron, posiblemente debido a permisos del sistema de archivos:\n\n", "u_blankf": "Estos {0} archivos (de un total de {1}) están en blanco / vacíos; ¿subirlos de todos modos?\n\n", "u_applef": "Estos {0} archivos (de un total de {1}) probablemente no son deseables;\nPulsa Aceptar/Intro para OMITIR los siguientes archivos,\nPulsa Cancelar/ESC para NO excluir, y SUBIR esos también:\n\n", "u_just1": "\nQuizás funcione mejor si seleccionas solo un archivo", "u_ff_many": "si usas Linux / MacOS / Android, esta cantidad de archivos podría bloquear Firefox!\nsi eso ocurre, por favor inténtalo de nuevo (o usa Chrome).", "u_up_life": "Esta subida será eliminada del servidor\n{0} después de que se complete", "u_asku": "subir estos {0} archivos a {1}", "u_unpt": "puedes deshacer / eliminar esta subida usando el 🧯 de arriba a la izquierda", "u_bigtab": "a punto de mostrar {0} archivos\n\nesto podría bloquear tu navegador, ¿estás seguro?", "u_scan": "Escaneando archivos...", "u_dirstuck": "el iterador de directorios se atascó intentando acceder a los siguientes {0} elementos; se omitirán:", "u_etadone": "Hecho ({0}, {1} archivos)", "u_etaprep": "(preparando para subir)", "u_hashdone": "hashing completado", "u_hashing": "hash", "u_hs": "negociando...", "u_started": "los archivos se están subiendo ahora; mira en [🚀]", "u_dupdefer": "duplicado; se procesará después de todos los demás archivos", "u_actx": "haz clic en este texto para evitar la pérdida de
          rendimiento al cambiar a otras ventanas/pestañas", "u_fixed": "¡OK!  Arreglado 👍", "u_cuerr": "fallo al subir el trozo {0} de {1};\nprobablemente inofensivo, continuando\n\narchivo: {2}", "u_cuerr2": "el servidor rechazó la subida (trozo {0} de {1});\nse reintentará más tarde\n\narchivo: {2}\n\nerror ", "u_ehstmp": "se reintentará; mira abajo a la derecha", "u_ehsfin": "el servidor rechazó la solicitud para finalizar la subida; reintentando...", "u_ehssrch": "el servidor rechazó la solicitud para realizar la búsqueda; reintentando...", "u_ehsinit": "el servidor rechazó la solicitud para iniciar la subida; reintentando...", "u_eneths": "error de red al realizar la negociación de subida; reintentando...", "u_enethd": "error de red al comprobar la existencia del destino; reintentando...", "u_cbusy": "esperando a que el servidor vuelva a confiar en nosotros después de un fallo de red...", "u_ehsdf": "¡el servidor se quedó sin espacio en disco!\n\nse seguirá reintentando, por si alguien\nlibera suficiente espacio para continuar", "u_emtleak1": "parece que tu navegador podría tener una fuga de memoria;\npor favor", "u_emtleak2": " cambia a https (recomendado) o ", "u_emtleak3": " ", "u_emtleakc": "prueba lo siguiente:\n
          • pulsa F5 para refrescar la página
          • luego desactiva el botón  mt  en los  ⚙️ ajustes
          • e intenta esa subida de nuevo
          Las subidas serán un poco más lentas, pero bueno.\n¡Perdón por las molestias!\n\nPD: chrome v107 tiene una solución para esto", "u_emtleakf": "prueba lo siguiente:\n
          • pulsa F5 para refrescar la página
          • luego activa 🥔 (ligera) en la interfaz de subida
          • e intenta esa subida de nuevo
          \nPD: firefox con suerte tendrá una solución en algún momento", "u_s404": "no encontrado en el servidor", "u_expl": "explicar", "u_maxconn": "la mayoría de los navegadores limitan esto a 6, pero firefox te permite aumentarlo con connections-per-server en about:config", "u_tu": '

          AVISO: turbo activado,  el cliente puede no detectar y reanudar subidas incompletas; ver tooltip del botón turbo

          ', "u_ts": '

          AVISO: turbo activado,  los resultados de búsqueda pueden ser incorrectos; ver tooltip del botón turbo

          ', "u_turbo_c": "turbo está desactivado en la configuración del servidor", "u_turbo_g": "desactivando turbo porque no tienes\nprivilegios para listar directorios en este volumen", "u_life_cfg": 'autoeliminar después de min (o horas)', "u_life_est": 'la subida se eliminará ---', "u_life_max": "esta carpeta impone una\nvida máxima de {0}", "u_unp_ok": "se permite deshacer la subida durante {0}", "u_unp_ng": "NO se permitirá deshacer la subida", "ue_ro": "tu acceso a esta carpeta es de solo lectura\n\n", "ue_nl": "actualmente no has iniciado sesión", "ue_la": "actualmente has iniciado sesión como \"{0}\"", "ue_sr": "actualmente estás en modo de búsqueda de archivos\n\ncambia a modo de subida haciendo clic en la lupa 🔎 (junto al gran botón BUSCAR), e intenta subir de nuevo\n\nlo siento", "ue_ta": "intenta subir de nuevo, ahora debería funcionar", "ue_ab": "este archivo ya se está subiendo a otra carpeta, y esa subida debe completarse antes de que el archivo pueda ser subido a otro lugar.\n\nPuedes abortar y olvidar la subida inicial usando el 🧯 de arriba a la izquierda", "ur_1uo": "OK: Archivo subido con éxito", "ur_auo": "OK: Todos los {0} archivos subidos con éxito", "ur_1so": "OK: Archivo encontrado en el servidor", "ur_aso": "OK: Todos los {0} archivos encontrados en el servidor", "ur_1un": "Subida fallida, lo siento", "ur_aun": "Todas las {0} subidas fallaron, lo siento", "ur_1sn": "El archivo NO se encontró en el servidor", "ur_asn": "Los {0} archivos NO se encontraron en el servidor", "ur_um": "Finalizado;\n{0} subidas OK,\n{1} subidas fallidas, lo siento", "ur_sm": "Finalizado;\n{0} archivos encontrados en el servidor,\n{1} archivos NO encontrados en el servidor", "rc_opn": "abrir", //m "rc_ply": "reproducir", //m "rc_pla": "reproducir como audio", //m "rc_txt": "abrir en el visor de archivos", //m "rc_md": "abrir en el editor de texto", //m "rc_dl": "descargar", //m "rc_zip": "descargar como archivo", //m "rc_cpl": "copiar enlace", //m "rc_del": "eliminar", //m "rc_cut": "cortar", //m "rc_cpy": "copiar", //m "rc_pst": "pegar", //m "rc_rnm": "renombrar", //m "rc_nfo": "nueva carpeta", //m "rc_nfi": "nuevo archivo", //m "rc_sal": "seleccionar todo", //m "rc_sin": "invertir selección", //m "rc_shf": "compartir esta carpeta", //m "rc_shs": "compartir selección", //m "lang_set": "¿refrescar para que el cambio surta efecto?", "splash": { "a1": "actualizar", "b1": "hola   (no has iniciado sesión)", "c1": "cerrar sesión", "d1": "volcar estado de la pila", "d2": "muestra el estado de todos los hilos activos", "e1": "recargar configuración", "e2": "recargar archivos de configuración (cuentas/volúmenes/indicadores de vol.),$Ny reescanear todos los volúmenes e2ds$N$Nnota: cualquier cambio en la configuración global$Nrequiere un reinicio completo para surtir efecto", "f1": "puedes explorar:", "g1": "puedes subir a:", "cc1": "otras cosas:", "h1": "desactivar k304", "i1": "activar k304", "j1": "activar k304 desconectará tu cliente en cada HTTP 304, lo que puede evitar que algunos proxies con errores se atasquen (dejando de cargar páginas de repente), pero también ralentizará las cosas en general", "k1": "restablecer config. de cliente", "l1": "inicia sesión para más:", "ls3": "iniciar sesión", //m "lu4": "nombre de usuario", //m "lp4": "contraseña", //m "lo3": "cerrar sesión de “{0}” en todas partes", //m "lo2": "esto finalizará la sesión en todos los navegadores", //m "m1": "bienvenido de nuevo,", "n1": "404 no encontrado  ┐( ´ -`)┌", "o1": '¿o quizás no tienes acceso? -- prueba con una contraseña o vuelve al inicio', "p1": "403 prohibido  ~┻━┻", "q1": 'usa una contraseña o vuelve al inicio', "r1": "ir al inicio", ".s1": "reescanear", "t1": "acción", "u2": "tiempo desde la última escritura en el servidor$N( subida / renombrar / ... )$N$N17d = 17 días$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos", "v1": "conectar", "v2": "usar este servidor como un disco duro local", "w1": "cambiar a https", "x1": "cambiar contraseña", "y1": "editar recursos compartidos", "z1": "desbloquear este recurso compartido:", "ta1": "primero escribe tu nueva contraseña", "ta2": "repite para confirmar la nueva contraseña:", "ta3": "hay un error; por favor, inténtalo de nuevo", "nop": "ERROR: La contraseña no puede estar vacía", //m "nou": "ERROR: El nombre de usuario y/o la contraseña no pueden estar vacíos", //m "aa1": "archivos entrantes:", "ab1": "desactivar no304", "ac1": "activar no304", "ad1": "activar no304 desactivará todo el almacenamiento en caché; prueba esto si k304 no fue suficiente. ¡Esto desperdiciará una gran cantidad de tráfico de red!", "ae1": "descargas activas:", "af1": "mostrar subidas recientes", "ag1": "mostrar usuarios IdP conocidos", } }; ================================================ FILE: copyparty/web/tl/swe.js ================================================ // Rader som slutar med //m är overifierade maskinöversättningar Ls.swe = { "tt": "Svenska", "cols": { "c": "aktion", "dur": "längd", "q": "kvalitet / bitrate", "Ac": "ljudkodek", "Vc": "videokodek", "Fmt": "format / container", "Ahash": "ljudchecksumma", "Vhash": "videochecksumma", "Res": "upplösning", "T": "filtyp", "aq": "ljudkvalitet / bitrate", "vq": "videokvalitet / bitrate", "pixfmt": "subsampling / pixelstruktur", "resw": "horisontell upplösning", "resh": "vertikal upplösning", "chs": "antal ljudkanaler", "hz": "samplingsfrekvens", }, "hks": [ [ "övrigt", ["ESC", "stäng diverse paneler"], "filhanterare", ["G", "växla mellan listvy / rutnät"], ["T", "växla mellan miniatyrer / ikoner"], ["⇧ A/D", "miniatyrstorlek"], ["ctrl-K", "radera urval"], ["ctrl-X", "klipp urval till urklipp"], ["ctrl-C", "kopiera urval till urklipp"], ["ctrl-V", "klistra in (kopiera/flytta) hit"], ["Y", "ladda ner urval"], ["F2", "byt namn på urval"], "välja filer", ["Blanksteg", "växla val av fil"], ["↑/↓", "flytta filvalsmarkör"], ["ctrl ↑/↓", "flytta markör och vy"], ["⇧ ↑/↓", "välj föregående/nästa fil"], ["ctrl-A", "välj alla filer / mappar"], ], [ "navigation", ["B", "växla mellan brödsmulor / trädvy"], ["I/K", "föregående/nästa mapp"], ["M", "hoppa till överordnad mapp (eller kollapsa närvarande)"], ["V", "växla mellan mappar / textfiler i trädvy"], ["A/D", "trädvystorlek"], ], [ "ljudspelare", ["J/L", "föregående/nästa låt"], ["U/O", "hoppa 10sek bakåt/framåt"], ["0..9", "hoppa till 0%..90%"], ["P", "play/paus (startar även uppspelning)"], ["S", "välj låten som spelas"], ["Y", "ladda ner låt"], ], [ "bildvisare", ["J/L, ←/→", "föregående/nästa bild"], ["Hem/End", "första/sista bilden"], ["F", "helskärm"], ["R", "rotera medsols"], ["⇧ R", "rotera motsols"], ["S", "välj bild"], ["Y", "ladda ner bild"], ], [ "videospelare", ["U/O", "hoppa 10sek bakåt/framåt"], ["P/K/Blanksteg", "play/paus"], ["C", "fortsätt med nästa"], ["V", "loopa"], ["M", "stäng av ljud"], ["[ och ]", "ställ in loopintervall"], ], [ "textfilsvisare", ["I/K", "föregående/nästa fil"], ["M", "stäng textfil"], ["E", "redigera textfil"], ["S", "välj fil"], ["Y", "ladda ner textfil"], //m ["⇧ J", "försköna json"], //m ] ], "m_ok": "OK", "m_ng": "Avbryt", "enable": "Aktivera", "danger": "VARNING", "clipped": "kopierat till urklipp", "ht_s1": "sekund", "ht_s2": "sekunder", "ht_m1": "minut", "ht_m2": "minuter", "ht_h1": "timme", "ht_h2": "timmar", "ht_d1": "dag", "ht_d2": "dagar", "ht_and": " och ", "goh": "kontrollpanel", "gop": 'föregående mapp">föreg.', "gou": 'överordnad mapp">upp', "gon": 'nästa mapp">nästa', "logout": "Logga ut ", "login": "Logga in", //m "access": "-rättighet", "ot_close": "stäng undermeny", "ot_search": "`sök efter filer via attribut, sökväg / namn, musiktaggar, eller någon kombination av dessa$N$N`foo bar` = måste innehålla både «foo» och «bar»,$N`foo -bar` = måste innehålla «foo» men inte «bar»,$N`^yana .opus$` = måste börja med «yana» och vara en «opus»-fil$N`"try unite"` = måste innehålla exakt «try unite»$N$Ndatumformatet är iso-8601, t.ex.$N`2009-12-31` eller `2020-09-12 23:30:00`", "ot_unpost": "unpost: radera dina senaste uppladdningar, eller avbryt pågående sådana", "ot_bup": "bup: enkel uppladdare, stödjer t o m netscape 4.0", "ot_mkdir": "mkdir: skapa en ny mapp", "ot_md": "new-file: skapa en ny textfil", "ot_msg": "msg: skicka ett meddelande till serverloggen", "ot_mp": "mediaspelarinställningar", "ot_cfg": "konfigurationsinställningar", "ot_u2i": 'up2k: ladda upp filer (om du har skrivrättigheter) eller byt till sökläge för att se om de finns någonstans på servern$N$Nuppladdningarna är återupptagbara, multitrådade och filernas tidsstämpel bevaras, men den använder mer CPU än [🎈]  (den enkla uppladdaren)

          under uppladdningens gång blir denna ikon en förloppsindikator!', "ot_u2w": 'up2k: ladda upp filer med stöd för återupptagning (stäng din webbläsare och dra in samma filer senare)$N$Nmultitrådad och filernas tidsstämpel bevaras, men den använder mer CPU än [🎈]  (den enkla uppladdaren)

          under uppladdningens gång blir denna ikon en förloppsindikator!', "ot_noie": 'Var vänlig använd Chrome / Firefox / Edge', "ab_mkdir": "skapa mapp", "ab_mkdoc": "ny textfil", "ab_msg": "skicka medd. till serverlogg", "ay_path": "hoppa till mappar", "ay_files": "hoppa till filer", "wt_ren": "byt namn på urval$NSnabbtangent: F2", "wt_del": "radera urval$NSnabbtangent: ctrl-K", "wt_cut": "klipp urval<small>(för att klistra in någonstans)</small>$NSnabbtangent: ctrl-X", "wt_cpy": "kopiera urval till urklipp$N(för att klistra in någonstans)$NSnabbtangent: ctrl-C", "wt_pst": "klistra in tidigare urval$NSnabbtangent: ctrl-V", "wt_selall": "välj alla filer$NSnabbtangent: ctrl-A (när en fil har fokus)", "wt_selinv": "invertera urval", "wt_zip1": "ladda ner denna mapp som ett arkiv", "wt_selzip": "ladda ner urval som ett arkiv", "wt_seldl": "ladda ner urval som separata filer$NSnabbtangent: Y", "wt_npirc": "kopiera IRC-formatterad låtinfo", "wt_nptxt": "kopiera låtinfo i klartext", "wt_m3ua": "lägg till i m3u-spellista (klicka på 📻copy senare)", "wt_m3uc": "kopiera m3u-spellista till urklipp", "wt_grid": "växla mellan rutnät och listvy$NSnabbtangent: G", "wt_prev": "föregående låt$NSnabbtangent: J", "wt_play": "play / paus$NSnabbtangent: P", "wt_next": "nästa låt$NSnabbtangent: L", "ul_par": "samtidiga uppladdningar:", "ut_rand": "slumpa filnamn", "ut_u2ts": "bevara tidsstämpeln för senaste ändring$Nfrån ditt filsystem till servern\">📅", "ut_ow": "skriv över existerande filer på servern?$N🛡️: aldrig (skapar ett nytt filnamn istället)$N🕒: skriv över om serverns fil är äldre än din$N♻️: skriv alltid över om filerna skiljer sig$N⏭️: hoppa ovillkorligen över alla befintliga filer", //m "ut_mt": "fortsätt hasha filer under uppladdningens gång$N$Nstäng av om din CPU eller disk är en flaskhals", "ut_ask": 'bekräfta innan uppladdningar påbörjas">💭', "ut_pot": "förbättra uppladdningshastigheten på långsamma enheter$Ngenom att förenkla användargränssnittet", "ut_srch": "ladda inte upp; kolla istället om filerna redan existerar på $N servern (detta kommer att skanna alla mappar med läsrättighet)", "ut_par": "du kan pausa all uppladdning genom att sätta detta till 0$N$Nöka denna om din uppkoppling är långsam eller har hög latens$N$Nsätt till 1 över lokala nätverk eller om serverns disk är en flaskhals", "ul_btn": "släpp filer / mappar
          här (eller klicka)", "ul_btnu": "L A D D A U P P", "ul_btns": "S Ö K", "ul_hash": "hashar", "ul_send": "skickar", "ul_done": "klar", "ul_idle1": "inga uppladdningar har köats", "ut_etah": "medelhastighet för <em>hashning</em>, och uppskattad återstående tid", "ut_etau": "medelhastighet för <em>överföring</em>, och uppskattad återstående tid", "ut_etat": "<em>total</em> medelhastighet, och uppskattad återstående tid", "uct_ok": "lyckade", "uct_ng": "no-good: misslyckade / avvisade / ej funna", "uct_done": "ok och ng kombinerat", "uct_bz": "pågående", "uct_q": "köade", "utl_name": "filnamn", "utl_ulist": "visa", "utl_ucopy": "kopiera", "utl_links": "länkar", "utl_stat": "status", "utl_prog": "förlopp", // keep short: "utl_404": "404", "utl_err": "FEL", "utl_oserr": "OS-fel", "utl_found": "hittad", "utl_defer": "väntar", "utl_yolo": "YOLO", "utl_done": "klar", "ul_flagblk": "filerna lades till i kön,
          men det finns en upptagen up2k i en annan webbläsarflik,
          så vi väntar på den först", "ul_btnlk": "serverkonfigurationen har låst denna inställning", "udt_up": "Ladda upp", "udt_srch": "Sök", "udt_drop": "släpp här", "u_nav_m": '
          jaha, vad har du då?
          Enter = Filer (en eller flera)\nESC = En mapp (inklusive undermappar)', "u_nav_b": 'FilerEn mapp', "cl_opts": "växlar", "cl_hfsz": "filstorlek", //m "cl_themes": "tema", "cl_langs": "språk", "cl_ziptype": "mappnedladdning", "cl_uopts": "up2k-inställningar", "cl_favico": "favikon", "cl_bigdir": "stora mappar", "cl_hsort": "#sort.", "cl_keytype": "tonartsnotering", "cl_hiddenc": "dolda kolumner", "cl_hidec": "dölj", "cl_reset": "återställ", "cl_hpick": "tryck på en kolumntitel för att dölja den i filvyn", "cl_hcancel": "kolumndöljning avbruten", "cl_rcm": "högerklicksmeny", //m "ct_grid": '田 rutnätet', "ct_ttips": '◔ ◡ ◔">ℹ️ tips', "ct_thumb": 'växla mellan miniatyrer och ikoner i rutnätsvyn$NSnabbtangent: T">🖼️ miniatyrer', "ct_csel": 'använd CTRL och SKIFT för urval av filer i rutnätsvyn">val', "ct_dsel": 'använd dra-urval i rutnätsvyn">dra', //m "ct_dl": 'tvinga nedladdning (visa inte inline) när en fil klickas">dl', //m "ct_ihop": 'skrolla till den senast visade filen när bildvisaren stängs">g⮯', "ct_dots": 'visa dolda filer (om servern tillåter detta)">dolda', "ct_qdel": 'bekräfta endast en gång när filer raderas">srad', "ct_dir1st": 'sortera mappar före filer">📁 först', "ct_nsort": 'naturlig sortering (för filnamn med ledande siffror)">nsort', "ct_utc": 'visa alla datum och tider i UTC">UTC', "ct_readme": 'visa README.md i listvyn">📜 läsmig', "ct_idxh": 'visa index.html istället för listvyn">htm', "ct_sbars": 'visa rullningslister">⟊', "cut_umod": "om en fil redan existerar på servern, uppdatera serverns senast modifierade tidsstämpel till att matcha din lokala fil (kräver skriv+radera-rättighet)\">re📅", "cut_turbo": "yolo-knappen, du vill förmodligen INTE aktivera denna:$N$Nanvänd denna om du höll på att ladda upp en stor mängd filer och var tvungen att stänga webbläsaren, och du vill fortsätta uppladdningen så fort som möjligt$N$Ndetta ersätter hash-checken med en enkel "har denna fil samma filstorlek på servern?", så om filinnehållet skiljer sig kommer den INTE att laddas upp$N$Ndu bör stänga av denna när uppladdningen är klar och sedan "ladda upp" samma filer igen för att låta klienten verifiera dem\">turbo", "cut_datechk": "har endast effekt med turbo-växeln påslagen$N$Nminskar yolo-faktorn lite grann; kollar om filtidsstämplarna på servern matchar dina$N$Ndetta bör fånga de flesta ofärdiga / korrumperade uppladdningarna, men kan inte ersätta ett fullständigt verifieringspass med turbo avstängt\">date-chk", "cut_u2sz": "storlek (i MiB) för varje uppladdnings-chunk; stora värden flyger bättre över atlanten. Prova lägre värden på mycket opålitliga uppkopplingar", "cut_flag": "garantera att endast en flik laddar upp samtidigt $N -- andra flikar måste också ha denna påslagen -- $N påverkar endast flikar på samma domän", "cut_az": "ladda upp filer i alfabetisk ordning, snarare än mindre filer först$N$Nalfabetisk ordning kan göra det enklare att se var något har gått fel, men uppladdningen blir lite långsammare över fiber / lokala nätverk", "cut_nag": "skicka en systemnotifikation när uppladdningar blir klara$N(endast om webbläsaren eller fliken inte är fokuserade)", "cut_sfx": "ljudnotifikation när uppladdningar blir klara$N(endast om webbläsaren eller fliken inte är fokuserade)", "cut_mt": "använd multitrådning för att accelerera filhashningen$N$Ndetta använder web-workers och kräver$Nmer RAM (upp till 512 MiB extra)$N$Nuppladdningar över https blir 30% snabbare, över http 4.5x snabbare\">mt", "cut_wasm": "använd wasm istället för webbläsarens inbyggda hashare; förbättrar hastigheten i chrome-baserade webbläsare men ökar CPU-lasten, och många äldre versioner av chrome har buggar som får webbläsaren att konsumera allt RAM-minne och krascha om detta är påslaget\">wasm", "cft_text": "favikon-text (låt stå tom och uppdatera sidan för att stänga av)", "cft_fg": "förgrundsfärg", "cft_bg": "bakgrundsfärg", "cdt_lim": "högsta antal filer att visa in en mapp", "cdt_ask": "när du når botten av vyn,$Nbe om en åtgärd istället för att ladda fler filer", "cdt_hsort": "`hur många sorteringsregler (`,sorthref`) att inkludera i media-URL:er. Sätts detta till 0 kommer regler i klickade medialänkar även att ignoreras", "cdt_ren": "aktivera anpassad högerklicksmeny, den vanliga menyn är tillgänglig med shift + högerklick\">aktivera", //m "cdt_rdb": "visa den vanliga högerklicksmenyn när den anpassade redan är öppen och man högerklickar igen\">x2", //m "tt_entree": "visa trädvy$NSnabbtangent: B", "tt_detree": "visa brödsmulor$NSnabbtangent: B", "tt_visdir": "skrolla till öppnad mapp", "tt_ftree": "växla mellan trädvy och textfiler$NHotkey: V", "tt_pdock": "visa överordnade mappar i en panel längst upp i vyn", "tt_dynt": "väx vyn när trädet expanderar", "tt_wrap": "automatisk radbrytning", "tt_hover": "visa överlånga rader när muspekaren hovrar över dem$N( skrollhjulet fungerar ej såvida inte pekaren$Nstår till vänster )", "ml_pmode": "vid mappens slut...", "ml_btns": "komm.", "ml_tcode": "konvertera", "ml_tcode2": "konvertera till", "ml_tint": "hy", "ml_eq": "ljudutjämnare", "ml_drc": "dynamikkompressor", "ml_ss": "hoppa över tystnad", //m "mt_loop": "upprepa en låt\">🔁", "mt_one": "stoppa uppspelningen efter en låt\">1️⃣", "mt_shuf": "blanda låtarna i varje mapp\">🔀", "mt_aplay": "spela automatiskt om det finns en låt-ID i länkar du har klickat på för att öppna sidan$N$Nom detta är avstängt kommer sidans adress inte att bli uppdaterad med en låt-ID om du spelar musik, för att förhindra automatisk uppspelning om dessa inställningar går förlorade men webbadressen återstår\">a▶", "mt_preload": "påbörja nedladdning av nästa låt i förväg för gapfri uppspelning\">ladda", "mt_prescan": "hoppa till nästa mapp i förväg så att webbläsaren$Nförblir glad och inte avbryter uppspelningen\">nav", "mt_fullpre": "försök att ladda ner hela låten i förväg;$N✅ aktivera på opålitliga uppkopplingar,$N❌ avaktivera kanske på långsamma uppkopplingar\">full", "mt_fau": "förhindra att uppspelningen avstannar på telefoner om nästa låt inte laddas tillräckligt snabbt i förväg (kan ge upphov till buggiga musiktaggar)\">☕️", "mt_waves": "vågformsreglage:$Nvisa ljudstyrkan i uppspelningsreglaget\">~s", "mt_npclip": "visa knappar för att kopiera låtinfo till urklippet\">/np", "mt_m3u_c": "visa knappar för att kopiera de valda$Nlåtarna som en m3u8-spellista\">📻", "mt_octl": "systemintegration (mediaknappar / skärmdisplay)\">os-ctl", "mt_oseek": "tillåt fram- och bakåtspolning via systemintegrationen$N$Nobs.: på vissa enheter (iPhone)$Nersätter detta knappen för nästa låt\">spola", "mt_oscv": "visa skivomslag i skärmdisplayen\">omslag", "mt_follow": "skrolla vyn till den spelande låten\">🎯", "mt_compact": "kompakt kontrollpanel\">⟎", "mt_uncache": "rensa cachen  (prova detta om din webbläsare har cachat$Nen trasig kopia av en låt och den vägrar spela upp den)\">rensa", "mt_mloop": "upprepa den öppna mappen\">🔁 upprepa", "mt_mnext": "ladda nästa mapp och fortsätt\">📂 nästa", "mt_mstop": "stoppa uppspelningen\">⏸ stopp", "mt_cflac": "konvertera flac / wav till {0}\">flac", "mt_caac": "konvertera aac / m4a till {0}\">aac", "mt_coth": "konvertera allt annat (förutom mp3) till {0}\">annat", "mt_c2opus": "bäst val för pc, laptop, android\">opus", "mt_c2owa": "opus-weba, för iOS 17.5 och senare\">owa", "mt_c2caf": "opus-caf, för iOS 11 till 17\">caf", "mt_c2mp3": "använd detta på mycket gamla enheter\">mp3", "mt_c2flac": "bäst ljudkvalitet, men enorma nedladdningar\">flac", "mt_c2wav": "okomprimerad uppspelning (ännu större)\">wav", "mt_c2ok": "snyggt, bra val", "mt_c2nd": "det är inte det rekommenderade formatet för din enhet, men det är lungt", "mt_c2ng": "din enhet verkar inte stödja det här formatet, men vi provar ändå", "mt_xowa": "det finns buggar i iOS som hindrar uppspelning i bakgrunden med detta format; vänligen använd caf eller mp3 istället", "mt_tint": "nivå på bakgrundsfärg (0-100) på uppspelningsreglaget;$Ngör buffring mindre distraherande", "mt_eq": "`aktiverar utjämning och förstärkning;$N$Nboost `0` = standard 100%-volym (omodifierad)$N$Nwidth `1  ` = standard stereo (omodifierad)$Nwidth `0.5` = 50% vänster-höger crossfeed$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = tar bort sång :^)$N$Nnär utjämningen är aktiverad blir gaplösa album verkligen gaplösa, så låt den stå påslagen med alla värden satta till 0 (förutom width = 1) om du bryr dig om det", "mt_drc": "aktiverar dynamikkompressorn (volymtillplattning / brickwaller); aktiverar även utjämnaren för att balansera röran, så sätt alla fält i utjämnaren förutom 'width' till 0 om du inte vill ha den$N$Nsänker all volym över THRESHOLD dB; för varje RATIO dB över THRESHOLD blir det 1 dB av output, så standardvärdena tresh = -24 och ratio = 12 innebär att volymen aldrig bör bli högre än -22 dB och det är säkert att höja utjämnarens boost till 0.8, eller t.o.m. 1.8 med ATK 0 och ett högt RLS-värde t.ex. 90 (fungerar endast i firefox; RLS är låst till högst 1 i andra webbläsare)$N$N(se wikipedia för en bättre förklaring)", "mt_ss": "`aktiverar tystnadshopp; multiplicerar uppspelningshastigheten med `sn` nära start/slut när volymen är under `vol` och positionen är inom första `sta`% eller sista `slt`%", //m "mt_ssvt": "volymtröskel (0-255)\">vol", //m "mt_ssts": "aktiv tröskel (% spår, start)\">sta", //m "mt_sste": "aktiv tröskel (% spår, slut)\">slt", //m "mt_sssm": "uppspelningshastighetsmultiplikator\">sn", //m "mb_play": "play", "mm_hashplay": "spela upp den här ljudfilen?", "mm_m3u": "tryck Enter/OK för att spela\ntryck ESC/Avbryt to Edit", "mp_breq": "firefox 82+ eller chrome 73+ eller iOS 15+ krävs", "mm_bload": "laddar...", "mm_bconv": "konverterar till {0}, vänligen vänta...", "mm_opusen": "din webbläsare kan inte spela upp aac- eller m4a-filer;\nkonvertering till opus är nu påslaget", "mm_playerr": "uppspelning misslyckades: ", "mm_eabrt": "Uppspelningen avbröts", "mm_enet": "Din uppkoppling är skum", "mm_edec": "Filen är korrumperad??", "mm_esupp": "Din webbläsare förstår inte detta format", "mm_eunk": "Okänt Fel", "mm_e404": "Kunde inte spela upp ljudfil; fel 404: Filen hittades inte.", "mm_e403": "Kunde inte spela upp ljudfil; fel 403: Åtkomst nekad.\n\nProva att ladda om sidan med F5, du kanske blev utloggad", "mm_e415": "Kunde inte spela upp ljudfil; fel 415: Filkonvertering misslyckades; kolla serverloggen.", //m "mm_e500": "Kunde inte spela upp ljudfil; fel 500: Kolla serverloggen.", "mm_e5xx": "Kunde inte spela upp ljudfil; serverfel ", "mm_nof": "hittade inga fler låtar i närheten", "mm_prescan": "Letar efter fler låtar...", "mm_scank": "Hittade nästa låt:", "mm_uncache": "cachen rensad; alla låtar kommer att laddas ner igen vid uppspelning", "mm_hnf": "den låten finns inte längre", "im_hnf": "den bilden finns inte längre", "f_empty": 'mappen är tom', "f_chide": 'detta kommer att dölja kolumnen «{0}»\n\ndu kan visa kolumner igen i inställningarna', "f_bigtxt": "den här filen är {0} MiB stor -- vill du verkligen visa den som text?", "f_bigtxt2": "visa endast slutet på filen? detta aktiverar även övervakning, vilket visar nya rader i filen i realtid", "fbd_more": '
          {0} av {1} filer visas; visa {2} eller visa alla
          ', "fbd_all": '
          {0} av {1} filer visas; visa alla
          ', "f_anota": "endast {0} av {1} objekt valdes;\nför att välja hela mappen, skrolla först till botten av vyn", "f_dls": 'fillänkarna i den öppna mappen har\nbytts till nedladdningslänkar', "f_dl_nd": 'hoppar över mapp (använd zip/tar-nedladdning istället):\n', //m "f_partial": "För att säkert ladda ner en fil som för tillfället laddas upp, vänligen klicka på filen som har samma filnamn men utan .PARTIAL-filändelsen. Vänligen tryck Avbryt eller Escape för att göra detta.\n\nOm du bortser från denna varning och trycker OK eller Enter kommer den tillfälliga .PARTIAL-filen istället att laddas ner, vilket är nästan garanterat att ge dig korrumperad data.", "ft_paste": "klistra in {0} objekt$NSnabbtangent: ctrl-V", "fr_eperm": 'kan ej byta namn:\ndu har inte flytträttighet i denna mapp', "fd_eperm": 'kan ej radera:\ndu har inte raderingsrättighet i denna mapp', "fc_eperm": 'kan ej klippa:\ndu har inte flytträttighet i denna mapp', "fp_eperm": 'kan ej klistra in:\ndu har inte skrivrättighet i denna mapp', "fr_emore": "välj minst en fil att byta namn på", "fd_emore": "välj minst en fil att radera", "fc_emore": "välj minst en fil att klippa", "fcp_emore": "välj minst en fil att kopiera till urklippet", "fs_sc": "dela den öppna mappen", "fs_ss": "dela de urvalda filerna", "fs_just1d": "du kan inte välja mer än en mapp\neller blanda filer och mappar i samma urval", "fs_abrt": "❌ avbryt", "fs_rand": "🎲 slump.namn", "fs_go": "✅ skapa utdelning", "fs_name": "namn", "fs_src": "källa", "fs_pwd": "lösen", "fs_exp": "utgång", "fs_tmin": "min", "fs_thrs": "timmar", "fs_tdays": "dagar", "fs_never": "oändlig", "fs_pname": "valfritt länknamn; slumpas fram om detta står tomt", "fs_tsrc": "filen eller mappen att dela", "fs_ppwd": "valfritt lösenord", "fs_w8": "skapar utdelning...", "fs_ok": "tryck Enter/OK för att kopiera länken till urklipp\ntryck ESC/Avbryt för att stänga", "frt_dec": "kan laga vissa typer av trasiga filnamn\">avkoda-url", "frt_rst": "återställ modifierade filnamn till de ursprungliga\">↺ återställ", "frt_abrt": "avbryt och stäng denna panel\">❌ avbryt", "frb_apply": "BYT NAMN", "fr_adv": "batch-, metadata- och mönsteromskrivning\">avancerat", "fr_case": "skiftlägeskänsligt reguljärt uttryck\">skift", "fr_win": "windows-säkra namn; ersätt <>:"\\|?* med japanska fullbreddtecken\">win", "fr_slash": "ersätt / med ett tecken som inte skapar nya mappar\">ingen /", "fr_re": "`reguljärt sökuttryck att tillämpa på ursprungliga filnamn; grupper kan hänvisas till i formatfältet nedan via `(1)` och `(2)` osv.", "fr_fmt": "`inspirerat av foobar2000:$N`(title)` ersätts av låttitel,$N`[(artist) - ](title)` skippar [detta] om artisten är tom$N`$lpad((tn),2,0)` fyller i spårnumret till 2 siffror", "fr_pdel": "ta bort", "fr_pnew": "spara som", "fr_pname": "ge ett nytt namn på din inställning", "fr_aborted": "avbrutet", "fr_lold": "gammalt namn", "fr_lnew": "nytt namn", "fr_tags": "taggar för de valda filerna (skrivskyddat, endast som referens):", "fr_busy": "byter namn på {0} objekt...\n\n{1}", "fr_efail": "namnbyte misslyckades:\n", "fr_nchg": "{0} av de nya namnen ändrades p g a win och/eller ingen /\n\nÄr det okej att fortsätta med de nya namnen?", "fd_ok": "radering lyckades", "fd_err": "radering misslyckades:\n", "fd_none": "inget raderades; kanske blockerat av serverkonfigurationen (xbd)?", "fd_busy": "raderar {0} objekt...\n\n{1}", "fd_warn1": "RADERA dessa {0} objekt?", "fd_warn2": "Sista chansen! Det finns inget sätt att ångra detta. Radera?", "fc_ok": "klippte {0} objekt", "fc_warn": 'klippte {0} objekt, men:\n\nendast denna webbläsarflik kan klistra in dem\n(eftersom urvalet är så enormt stort)', "fcc_ok": "kopierade {0} objekt till urklippet", "fcc_warn": 'kopierade {0} objekt till urklippet, men:\n\nendast denna webbläsarflik kan klistra in dem\n(eftersom urvalet är så enormt stort)', "fp_apply": "använd dessa namn", "fp_skip": "skippa upptagna", //m "fp_ecut": "klipp eller kopiera filer / mappar först för att klistra / flytta dem\n\nobs.: du kan klippa och klistra mellan webbläsarflikar", "fp_ename": "{0} objekt kan ej flyttas hit eftersom filnamnen redan är tagna. Ge dem nya namn nedan för att fortsätta, eller lämna fältet tomt (\"skippa upptagna\") för att skippa:", //m "fcp_ename": "{0} objekt kan ej kopieras hit eftersom filnamnen redan är tagna. Ge dem nya namn nedan för att fortsätta, eller lämna fältet tomt (\"skippa upptagna\") för att skippa:", //m "fp_emore": "det finns fortfarande filnamnskrockar att fixa", "fp_ok": "flytt lyckades", "fcp_ok": "kopiering lyckades", "fp_busy": "flyttar {0} objekt...\n\n{1}", "fcp_busy": "kopierar {0} objekt...\n\n{1}", "fp_abrt": "avbryter...", "fp_err": "flytt misslyckades:\n", "fcp_err": "kopiering misslyckades:\n", "fp_confirm": "flytta dessa {0} objekt hit?", "fcp_confirm": "kopiera dessa {0} objekt hit?", "fp_etab": 'lyckades ej läsa urklippet från en annan webbläsarflik', "fp_name": "laddar upp en fil från din enhet. Ge den ett namn:", "fp_both_m": '
          välj vad som ska klistras in
          Enter = Flytta {0} objekt från «{1}»\nESC = Ladda upp {2} filer från din enhet', "fcp_both_m": '
          välj vad som ska klistras in
          Enter = Kopiera {0} objekt från «{1}»\nESC = Ladda upp {2} filer från din enhet', "fp_both_b": 'FlyttaLadda upp', "fcp_both_b": 'KopieraLadda upp', "mk_noname": "skriv ett namn i fältet till vänster först :p", "nmd_i1": "lägg också till filändelsen du vill ha, till exempel .md", //m "nmd_i2": "du kan bara skapa .{0}-filer eftersom du inte har borttagningsbehörighet", //m "tv_load": "Laddar textfil:\n\n{0}\n\n{1}% ({2} av {3} MiB laddat)", "tv_xe1": "kunde ej ladda textfil:\n\nfel ", "tv_xe2": "404, filen hittades inte", "tv_lst": "lista av textfiler i", "tvt_close": "återvänd till mapp$NSnabbtangent: M (eller Esc)\">❌ stäng", "tvt_dl": "ladda ner denna fil$NSnabbtangent: Y\">💾 ladda ner", "tvt_prev": "visa föregående fil$NSnabbtangent: i\">⬆ föreg.", "tvt_next": "visa nästa fil$NSnabbtangent: K\">⬇ nästa", "tvt_sel": "välj fil   ( för klipp / kopiera / radera / ... )$NSnabbtangent: S\">välj", "tvt_j": "försköna json$NSnabbtangent: shift-J\">j", //m "tvt_edit": "öppna fil i textredigerare$NSnabbtangent: E\">✏️ redigera", "tvt_tail": "övervaka filen; visa nya rader i realtid\">📡 övervaka", "tvt_wrap": "automatisk radbrytning\">↵", "tvt_atail": "lås vyn till sidans botten\">⚓", "tvt_ctail": "avkoda terminalfärger (ansi-escapesekvenser)\">🌈", "tvt_ntail": "gräns för scrollback (hur många byte ska behållas laddade)", "m3u_add1": "låt tillagd till m3u-spellista", "m3u_addn": "{0} låtar tillagda till m3u-spellista", "m3u_clip": "m3u-spellista kopierad till urklippet\n\ndu bör skapa en ny textfil som heter någonting.m3u och klistra in spellistan i det dokumentet; detta gör den uppspelbar", "gt_vau": "visa inte videor, spela endast ljudet\">🎧", "gt_msel": "urval av filer; ctrl-klicka en fil för standardbeteende$N$N<em>när detta är aktiverat: dubbelklicka en fil / mapp för att öppna den</em>$N$NSnabbtangent: S\">urval", "gt_crop": "centrera och beskär miniatyrbilder\">beskär", "gt_3x": "högupplösta miniatyrbilder\">3x", "gt_zoom": "zoom", "gt_chop": "klipp", "gt_sort": "sortera efter", "gt_name": "namn", "gt_sz": "storlek", "gt_ts": "datum", "gt_ext": "typ", "gt_c1": "förkorta filnamn (visa mindre)", "gt_c2": "förläng filnamn (visa mer)", "sm_w8": "söker...", "sm_prev": "sökresultaten nedan är från en tidigare sökning:\n ", "sl_close": "stäng sökresultaten", "sl_hits": "visar {0} träffar", "sl_moar": "ladda fler", "s_sz": "storlek", "s_dt": "datum", "s_rd": "sökväg", "s_fn": "namn", "s_ta": "taggar", "s_ua": "uppl.", "s_ad": "avanc.", "s_s1": "minimum MiB", "s_s2": "maximum MiB", "s_d1": "min. iso8601", "s_d2": "max. iso8601", "s_u1": "uppladdad efter", "s_u2": "och/eller före", "s_r1": "sökvägen innehåller   (blankstegsseparerat)", "s_f1": "filnamnet innehåller   (invertera med -intedetta)", "s_t1": "taggar innehåller   (^=start, slut=$)", "s_a1": "specifika metadataegenskaper", "md_eshow": "kan ej visa ", "md_off": "[📜läsmig] avstängt i [⚙️] -- dokumentet är dolt", "badreply": "Kunde ej tolka svaret från servern", "xhr403": "403: Åtkomst nekad\n\nProva att ladda om sidan med F5, du kanske blev utloggad", "xhr0": "okänt (tappade förmodligen kontakt med servern, eller så är den nere)", "cf_ok": "ledsen -- DD" + wah + "oS-skyddet har aktiverats\n\nsaker bör fungera igen om 30 sekunder\n\nom inget händer, ladda om sidan med F5", "tl_xe1": "kunde inte visa undermappar:\n\nfel ", "tl_xe2": "404: Mappen hittades inte", "fl_xe1": "kunde inte visa filer i mapp:\n\nfel ", "fl_xe2": "404: Mappen hittades inte", "fd_xe1": "kunde inte skapa mapp:\n\nfel ", "fd_xe2": "404: Överordnad mapp hittades inte", "fsm_xe1": "kunde inte skicka meddelande:\n\ndel ", "fsm_xe2": "404: Överordnad mapp hittades inte", "fu_xe1": "kunde inte ladda unpost-listan från servern:\n\nfel ", "fu_xe2": "404: Filen hittades inte??", "fz_tar": "okomprimerad tar-fil i gnu-format (linux / mac)", "fz_pax": "okomprimerad tar-fil i pax-format (långsammare)", "fz_targz": "gnu-tar komprimerad med gzip-nivå 3$N$Ndetta är vanligtvis mycket långsamt,$Nanvänd okomprimerad tar istället", "fz_tarxz": "gnu-tar komprimerad med xz-nivå 1$N$Ndetta är vanligtvis mycket långsamt,$Nanvänd okomprimerad tar istället", "fz_zip8": "zip-fil med utf8-filnman (kan vara skum i windows 7 och äldre)", "fz_zipd": "zip-fil med standard cp437-filnamn, för riktigt gammal mjukvara", "fz_zipc": "cp437 med crc32 uträknad i förväg,$Nför MS-DOS PKZIP v2.04g (oktober 1993)$N(tar längre tid att behandla innan nedladdningen kan påbörjas)", "un_m1": "du kan radera dina senaste uppladdningar (eller avbryta pågående sådana) nedan", "un_upd": "uppdatera", "un_m4": "eller, dela filerna som syns nedan:", "un_ulist": "visa", "un_ucopy": "kopiera", "un_flt": "filter:  sökvägen måste innehålla", "un_fclr": "rensa filtret", "un_derr": 'unpost-radering misslyckades:\n', "un_f5": 'något gick sönder, prova att uppdatera eller tryck på F5', "un_uf5": "ledsen men du måste uppdatera sidan (t.ex. genom att trycka på F5 eller CTRL-R) innan du kan avbryta den här uppladdningen", "un_nou": 'varning: servern är för upptagen för att visa pågående uppladdningar; klicka på "uppdatera" om en stund', "un_noc": 'varning: serverkonfigurationen tillåter inte unpost:ning av uppladdade filer', "un_max": "visar de första 2000 filerna (använd filtret)", "un_avail": "{0} av de senaste uppladdningarna kan raderas
          {1} pågående uppladdningar kan avbrytas", "un_m2": "sorterat efter uppladdningstid; senast uppladdad först:", "un_no1": "tjosan! inga uppladdningar är tillräckligt nya", "un_no2": "tjosan! inga uppladdningar som matchar filtret är tillräckligt nya", "un_next": "radera de {0} nästkommande filerna", "un_abrt": "avbryt", "un_del": "radera", "un_m3": "laddar dina senaste uppladdningar...", "un_busy": "raderar {0} filer...", "un_clip": "{0} länkar kopierade till urklippet", "u_https1": "du bör", "u_https2": "byta till https", "u_https3": "för bättre prestanda", "u_ancient": 'din webbläsare är imponerande uråldrig -- du kanske borde använda bup istället', "u_nowork": "firefox 53+ eller chrome 57+ eller iOS 11+ krävs", "tail_2old": "firefox 105+ eller chrome 71+ eller iOS 14.5+ krävs", "u_nodrop": 'din webbläsare är för gammal för dra-och-släpp-uppladdning', "u_notdir": "det där är ingen mapp!\n\ndin webbläsare är för gammal,\nprova dra-och-släpp istället", "u_uri": "släpp bilder från andra webbläsarfönster på den stora\nuppladdningsknappen för att ladda upp dem", "u_enpot": 'byt till potatisgränssnittet (kan förbättra uppladdningshastigheten)', "u_depot": 'byt till det snygga gränssnittet (kan försämra uppladdningshastigheten)', "u_gotpot": 'byter till potatisgränssnittet för förbättrad uppladdningshastighet,\n\nbyt gärna tillbaka om du vill!', "u_pott": "

          filer:   {0} färdiga,   {1} misslyckade,   {2} pågående,   {3} köade

          ", "u_ever": "detta är den enkla uppladdaren; up2k kräver minst
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'detta är den enkla uppladdaren; up2k är bättre', "u_uput": 'optimera hastigheten (skippa checksumman)', "u_ewrite": 'du har inte skrivrättighet i denna mapp', "u_eread": 'du har inte läsrättighet i denna mapp', "u_enoi": 'serverkonfigurationen har inte slagit på sökning', "u_enoow": "du kan inte skriva över här; raderingsrättighet krävs", "u_badf": 'Dessa {0} filer (av totalt {1}) skippades, möjligtvis p.g.a. filsystemsrättigheter:\n\n', "u_blankf": 'Dessa {0} filer (av totalt {1}) är tomma; ladda upp dem ändå?\n\n', "u_applef": 'Dessa {0} filer (av totalt {1}) är förmodligen oönskade;\nTryck OK/Enter för att SKIPPA de följande filerna,\nTryck Avbryt/ESC för att INKLUDERA och LADDA UPP dem:\n\n', "u_just1": '\nDet kanske fungerar om du endast väljer en fil', "u_ff_many": "om du använder Linux / MacOS / Android,kan denna mängd filer krascha Firefox!\nom detta händer, vänligen försök igen (eller använd Chrome).", "u_up_life": "Denna uppladdning kommer att raderas från servern om\n{0} efter att den har blivit uppladdad", "u_asku": 'ladda upp dessa {0} filer till {1}', "u_unpt": "du kan ångra / radera denna uppladdning med 🧯 uppe till vänster", "u_bigtab": 'försöker att visa {0} filer\n\ndetta kan krascha din webbläsare, är du säker?', "u_scan": 'Scannar filer...', "u_dirstuck": 'katalogskannern fastnade när den försökte komma åt de följande {0} objekten; dessa kommer att skippas:', "u_etadone": 'Klar ({0}, {1} filer)', "u_etaprep": '(förbereder uppladdning)', "u_hashdone": 'hashning klar', "u_hashing": 'hashar', "u_hs": 'skakar hand...', "u_started": "filerna laddas nu upp; se [🚀]", "u_dupdefer": "duplikat; kommer att behandlas efter alla andra filer", "u_actx": "klicka här för att undvika prestandaförlust
          när du byter till andra fönster/flikar", "u_fixed": "Okej!  Fixat 👍", "u_cuerr": "misslyckades att ladda upp chunk {0} av {1};\nförmodligen harmlöst, fortsätter\n\nfil: {2}", "u_cuerr2": "servern avvisade uppladdningen (chunk {0} av {1});\nprovar igen senare\n\nfil: {2}\n\nfel ", "u_ehstmp": "provar igen; see nedåt till höger", "u_ehsfin": "servern avvisade förfrågan att färdigställa uppladdningen; provar igen...", "u_ehssrch": "servern avvisade förfrågan att söka; provar igen...", "u_ehsinit": "servern avvisade förfrågan att påbörja uppladdningen; provar igen...", "u_eneths": "nätverksfel vid handskakning; provar igen...", "u_enethd": "nätverksfel när destinationens existens testades; provar igen...", "u_cbusy": "väntar på att servern ska lita på oss igen efter nätverksfel...", "u_ehsdf": "servern fick slut på diskutrymme!\n\nprovar igen, ifall någon rensar upp\ntillräckligt med utrymme för att fortsätta", "u_emtleak1": "det verkar som att din webbläsare kanske har en minnesläcka;\nvänligen", "u_emtleak2": ' byt till https (rekommenderat) eller ', "u_emtleak3": ' ', "u_emtleakc": 'prova följande:\n
          • tryck F5 för att uppdatera sidan
          • avaktivera sedan  mt -växeln i  ⚙️-inställningarna
          • och prova att ladda upp igen
          Uppladdningar kommer att vara lite långsammare, men aja.\nBeklagar problemet!\n\nPS: chrome v107 har en buggfix för detta', "u_emtleakf": 'prova följande:\n
          • tryck F5 för att uppdatera sidan
          • aktivera sedan 🥔 (potatis) i uppladdningsgränssnittet
          • och prova att ladda upp igen
          \nPS: firefox kommer förhoppningsvis få en buggfix vid något tillfälle', "u_s404": "hittades ej på servern", "u_expl": "förklara", "u_maxconn": "de flesta webbläsare begränsar detta till 6, men firefox låter dig höja gränsen med connections-per-server i about:config", "u_tu": '

          VARNING: turbo är aktiverat,  det är möjligt att klienten inte upptäcker och återupptar ofärdiga uppladdningar; se tipset för turbo-växeln

          ', "u_ts": '

          VARNING: turbo är aktiverat,  sökresultat kan vara felaktiga; se tipset för turbo-växeln

          ', "u_turbo_c": "serverkonfigurationen har avaktiverat turbo", "u_turbo_g": "avaktiverar turbo eftersom du inte har rättigheten\natt se mappars innehåll i den här volymen", "u_life_cfg": 'radera automatiskt efter min (eller timmar)', "u_life_est": 'uppladdningen kommer att raderas vid ---', "u_life_max": 'denna mapp tvingar en\nhögsta livstid på {0}', "u_unp_ok": 'unpost är tillåten för {0}', "u_unp_ng": 'unpost är INTE tillåten', "ue_ro": 'du har endast läsrättighet till denna mapp\n\n', "ue_nl": 'du är inte inloggad', "ue_la": 'du är inloggad som "{0}"', "ue_sr": 'du är i filsökläge\n\nbyt till uppladdningsläge genom att klicka på förstoringsglaset 🔎 (bredvid den stora SÖK-knappen), och försök ladda upp igen\n\nledsen', "ue_ta": 'prova att ladda upp igen nu, det bör fungera', "ue_ab": "denna fil laddas redan upp till en annan mapp, och den uppladdningen måste färdigställas innan filen kan laddas upp någon annanstans.\n\nDu kan avbryta och glömma bort den uppladdningen med 🧯 uppe till vänster", "ur_1uo": "Okej: Filen laddades upp med framgång", "ur_auo": "Okej: Alla {0} filer laddades upp med framgång", "ur_1so": "Okej: Filen fanns på servern", "ur_aso": "Okej: Alla {0} filer fanns på servern", "ur_1un": "Uppladdningen misslyckades, ledsen", "ur_aun": "Alla {0} uppladdningar misslyckades, ledsen", "ur_1sn": "Filen hittades INTE på servern", "ur_asn": "De {0} filerna hittades INTE på servern", "ur_um": "Klar;\n{0} uppladdningar gick okej,\n{1} uppladdningar misslyckades, ledsen", "ur_sm": "Klar;\n{0} filer hittades på servern,\n{1} filer hittades INTE på servern", "rc_opn": "öppna", //m "rc_ply": "spela upp", //m "rc_pla": "spela upp som ljud", //m "rc_txt": "öppna i filvisare", //m "rc_md": "öppna i textredigerare", //m "rc_dl": "Ladda ner", //m "rc_zip": "Ladda ner som arkiv", //m "rc_cpl": "kopiera länk", //m "rc_del": "radera", //m "rc_cut": "klipp ut", //m "rc_cpy": "kopiera", //m "rc_pst": "klistra in", //m "rc_rnm": "byt namn", //m "rc_nfo": "ny mapp", //m "rc_nfi": "ny fil", //m "rc_sal": "markera alla", //m "rc_sin": "invertera markering", //m "rc_shf": "dela denna mapp", //m "rc_shs": "dela urval", //m "lang_set": "uppdatera för att ändringen ska ta effekt?", "splash": { "a1": "uppdatera", "b1": "tjena främling   (du är inte inloggad)", "c1": "logga ut", "d1": "dumpa stacken", "d2": "visar tillståndet på alla aktiva trådar", "e1": "ladda om konfig.", "e2": "ladda om konfigurationsfiler (konton/volymer/volflaggor),$Noch skanna om alla e2ds-volymer$N$Nobs.: ändrade globala inställningar$Nkräver en fullständig omstart", "f1": "du kan bläddra:", "g1": "du kan ladda upp till:", "cc1": "annat:", "h1": "avaktivera k304", "i1": "aktivera k304", "j1": "med k304 aktiverad kommer klienten att koppla bort sig vid varje HTTP 304-fel, vilket kan hindra vissa buggiga proxyservrar från att fastna (sidor slutar ladda), men saker kommer också att bli långsammare i allmänhet", "k1": "återställ klientinställningar", "l1": "logga in för att se mer:", "ls3": "logga in", //m "lu4": "användarnamn", //m "lp4": "lösenord", //m "lo3": "logga ut “{0}” överallt", //m "lo2": "avsluta sessionen i alla webbläsare", //m "m1": "välkommen tillbaka,", "n1": "404 hittades inte  ┐( ´ -`)┌", "o1": 'eller så har du kanske inte tillgång -- prova ett lösenord eller åk hem', "p1": "403 nekat  ~┻━┻", "q1": 'använd ett lösenord eller åk hem', "r1": "åk hem", ".s1": "skanna om", "t1": "åtgärd", "u2": "tid sedan senaste serverskrivning$N( uppladdning / namnbyte / ... )$N$N17d = 17 dagar$N1h23 = 1 timme 23 minuter$N4m56 = 4 minuter 56 sekunder", "v1": "koppla upp", "v2": "använd denna server som en lokal disk", "w1": "byt till https", "x1": "byt lösenord", "y1": "redigera utdelningar", "z1": "lås upp denna utdelning:", "ta1": "fyll i ditt nya lösenord", "ta2": "upprepa det nya lösenordet:", "ta3": "det blev fel; vänligen försök igen", "nop": "FEL: Lösenordet får inte vara tomt", //m "nou": "FEL: Användarnamn och/eller lösenord får inte vara tomt", //m "aa1": "inkommande filer:", "ab1": "avaktivera no304", "ac1": "aktivera no304", "ad1": "detta stänger av all cachning; prova detta om k304 inte räckte till. Detta kommer att slösa enorma mängder nätverkstrafik!", "ae1": "aktiva nedladdningar:", "af1": "visa senaste uppladdningar", "ag1": "visa idp-cache", } }; ================================================ FILE: copyparty/web/tl/tur.js ================================================ // "//m" ile biten satırlar doğrulanmamış makine çevirileridir Ls.tur = { "tt": "Türkçe", "cols": { "c": "işlem butonları", "dur": "süre", "q": "kalite / bitrate", "Ac": "ses kodlaması", "Vc": "video kodlaması", "Fmt": "format / yapı", "Ahash": "ses denetim toplamı", "Vhash": "video denetim toplamı", "Res": "çözünürlük", "T": "dosya türü", "aq": "ses kalitesi / bitrate", "vq": "video kalitesi / bitrate", "pixfmt": "subsampling / pixel yapısı", "resw": "yatay çözünürlük", "resh": "dikey çözünürlük", "chs": "ses kanalları", "hz": "örnekleme hızı", }, "hks": [ [ "diğer", ["ESC", "kapat"], "dosya yönetimi", ["G", "liste / ızgara görünümü arasında geçiş yap"], ["T", "küçük resimler / simgeler arasında geçiş yap"], ["⇧ A/D", "küçük resim boyutu"], ["ctrl-K", "seçileni sil"], ["ctrl-X", "seçimi panoya kes"], ["ctrl-C", "seçimi panoya kopyala"], ["ctrl-V", "buraya yapıştır (taşı/kopyala)"], ["Y", "seçileni indir"], ["F2", "seçileni yeniden adlandır"], "dosya yönetimi seçimleri", ["boşluk", "seçimi değiştir"], ["↑/↓", "seçim imlecini hareket ettir"], ["ctrl ↑/↓", "imleci ve görünümü hareket ettir"], ["⇧ ↑/↓", "önceki/sonraki dosyayı seç"], ["ctrl-A", "tüm dosyaları / klasörleri seç"], ], [ "navigasyon", ["B", "içerik haritası / navigasyon paneli arasında geçiş yap"], ["I/K", "önceki/sonraki klasör"], ["M", "üst klasör (veya mevcut olanı daralt)"], ["V", "navigasyon panelinde klasörler / metin dosyaları arasında geçiş yap"], ["A/D", "navigasyon paneli boyutu"], ], [ "ses oynatıcı", ["J/L", "önceki/sonraki şarkı"], ["U/O", "10sn geri/ileri atla"], ["0..9", "%0..%90 atla"], ["P", "oynat/duraklat (aynı zamanda oynatıcıyı aç)"], ["S", "şarkıyı seç"], ["Y", "şarkıyı indir"], ], [ "resim görüntüleyici", ["J/L, ←/→", "önceki/sonraki resim"], ["Home/End", "ilk/son resim"], ["F", "tam ekran"], ["R", "sağa döndür"], ["⇧ R", "sola döndür"], ["S", "resmi seç"], ["Y", "resmi indir"], ], [ "video oynatıcı", ["U/O", "10sn geri/ileri atla"], ["P/K/Space", "oynat/duraklat"], ["C", "sıradakinden devam et"], ["V", "döngü"], ["M", "sessiz"], ["[ ve ]", "döngü aralığını ayarla"], ], [ "metin dosyası görüntüleyici", ["I/K", "önceki/sonraki dosya"], ["M", "metin dosyasını kapat"], ["E", "metin dosyasını düzenle"], ["S", "dosyayı seç (kes/kopyala/yeniden adlandır)"], ["Y", "metin dosyasını indir"], //m ["⇧ J", "json güzelleştir"], //m ] ], "m_ok": "Tamam", "m_ng": "İptal", "enable": "Etkinleştir", "danger": "TEHLİKE", "clipped": "panoya kopyalandı", "ht_s1": "saniye", "ht_s2": "saniye", "ht_m1": "dakika", "ht_m2": "dakika", "ht_h1": "saat", "ht_h2": "saat", "ht_d1": "gün", "ht_d2": "gün", "ht_and": " ve ", "goh": "kontrol paneli", "gop": 'önceki kardeş">önceki', "gou": 'üst klasör">üst', "gon": 'sonraki klasör">sonraki', "logout": "Çıkış ", "login": "Giriş", "access": " erişim", "ot_close": "alt menüyü kapat", "ot_search": "`dosyaları özniteliklere, yol / ad, müzik etiketlerine veya bunların herhangi bir kombinasyonuna göre arayın$N$N`foo bar` = hem «foo» hem de «bar» içermelidir,$N`foo -bar` = «foo» içermeli ancak «bar» içermemelidir,$N`^yana .opus$` = «yana» ile başlamalı ve bir «opus» dosyası olmalıdır$N`"try unite"` = tam olarak «try unite» içermelidir$N$N tarih formatı iso-8601'dir, gibi$N`2009-12-31` veya `2020-09-12 23:30:00`", "ot_unpost": "unpost: son yüklemelerinizi silin veya tamamlanmamış olanları iptal edin", "ot_bup": "bup: temel yükleyici, hatta netscape 4.0'ı destekler", "ot_mkdir": "mkdir: yeni bir dizin oluştur", "ot_md": "new-file: yeni bir metin dosyası oluştur", //m "ot_msg": "msg: sunucu günlüğüne bir mesaj gönder", "ot_mp": "medya oynatıcı seçenekleri", "ot_cfg": "konfigürasyon seçenekleri", "ot_u2i": 'up2k: dosyaları yükle (yazma erişiminiz varsa) veya sunucuda bir yerde var olup olmadıklarını görmek için arama moduna geçiş yap$N$Nyüklemeler devam ettirilebilir, çok iş parçacıklı ve dosya zaman damgaları korunur, ancak [🎈]  (temel yükleyici) ile karşılaştırıldığında daha fazla CPU kullanır

          yükleme sırasında, bu simge bir ilerleme göstergesi haline gelir!', "ot_u2w": 'up2k: devam desteği ile dosyaları yükle (tarayıcınızı kapatın ve daha sonra aynı dosyaları bırakın)$N$Nçok iş parçacıklı ve dosya zaman damgaları korunur, ancak [🎈]  (temel yükleyici) ile karşılaştırıldığında daha fazla CPU kullanır

          yükleme sırasında, bu simge bir ilerleme göstergesi haline gelir!', "ot_noie": 'Lütfen Chrome / Firefox / Edge kullanın', "ab_mkdir": "dizin oluştur", "ab_mkdoc": "yeni metin dosyası", //m "ab_msg": "sunucu günlüğüne mesaj gönder", "ay_path": "klasörlere atla", "ay_files": "dosyalara atla", "wt_ren": "seçilenleri yeniden adlandır$NKısa yol: F2", "wt_del": "Seçilen ögeleri sil$Kısa yol: ctrl-K", "wt_cut": "seçilen ögeleri kes <small></small>$NKısa yol: ctrl-X", "wt_cpy": "seçilen ögeleri panoya kopyala$N$NKısa yol: ctrl-C", "wt_pst": "daha önce kesilmiş / kopyalanmış bir seçimi yapıştır$NKısa yol: ctrl-V", "wt_selall": "tüm dosyaları seç$NKısa yol: ctrl-A (dosya odaklandığında)", "wt_selinv": "seçimi tersine çevir", "wt_zip1": "seçilenleri sıkıştırılmış bir arşiv olarak indir", "wt_selzip": "seçimi arşiv olarak indir", "wt_seldl": "seçimi ayrı dosyalar olarak indir$NHotkey: Y", "wt_npirc": "irc biçiminde parça bilgilerini kopyala", "wt_nptxt": "düz metin parça bilgilerini kopyala", "wt_m3ua": "m3u çalma listesine ekle (daha sonra 📻kopyalaya tıklayın)", "wt_m3uc": "m3u çalma listesini panoya kopyala", "wt_grid": "ızgara / liste görünümünü değiştir$NHotkey: G", "wt_prev": "önceki parça$NHotkey: J", "wt_play": "oynat / duraklat$NHotkey: P", "wt_next": "sonraki parça$NHotkey: L", "ul_par": "paralel yüklemeler:", "ut_rand": "dosya adlarını rastgeleleştir", "ut_u2ts": "kendi dosyalarınızdan sunucuya$Nzaman damgasını kopyala\">📅", "ut_ow": "sunucudaki mevcut dosyaları üzerine yazmak mı?$N🛡️: asla (yerine yeni bir dosya adı oluşturur)$N🕒: sunucu dosyası sizinkinden daha eskiyse üzerine yaz$N♻️: dosyalar farklıysa her zaman üzerine yaz$N⏭️: mevcut tüm dosyaları koşulsuz atla", //m "ut_mt": "yükleme yaparken diğer dosyaların hash'lenmesini durdur$N$kötü bir CPU veya HDD'ye sahipseniz kullanabilirsiniz.", "ut_ask": 'yüklemeye başlamadan önce doğrulama mesajı göster">💭', "ut_pot": "arayüzü daha az karmaşık hale getirerek$Nyükleme hızını yavaş cihazlarda artır", "ut_srch": "gerçekten yükleme yapma, bunun yerine dosyaların $N sunucuda var olup olmadığını kontrol et (okuma izniniz olan tüm klasörleri tarar)", "ut_par": "0'a ayarlayarak yüklemeleri durdur$N$Nbağlantınız yavaşsa değeri artırın$N$NLAN'daysanız veya sunucu HDD'si darboğaz yapıyorsa 1'de tutun", "ul_btn": "dosyaları / klasörleri
          buraya sürükleyin (veya bana tıklayın)", "ul_btnu": "Y Ü K L E", "ul_btns": "A R A", "ul_hash": "hash", "ul_send": "gönder", "ul_done": "tamamlandı", "ul_idle1": "henüz bekleyen yükleme yok", "ut_etah": "ortalama <em>hash</em> hızı ve bitişe kadar tahmini süre", "ut_etau": "ortalama <em>yükleme</em> hızı ve bitişe kadar tahmini süre", "ut_etat": "ortalama <em>toplam</em> hızı ve bitişe kadar tahmini süre", "uct_ok": "başarıyla tamamlandı", "uct_ng": "başarısız: başarısız / reddedildi / bulunamadı", "uct_done": "hem başarılı hem de başarısız", "uct_bz": "hash'liyor veya yüklüyor", "uct_q": "beklemede, gönderiliyor", "utl_name": "dosya adı", "utl_ulist": "liste", "utl_ucopy": "kopyala", "utl_links": "bağlantılar", "utl_stat": "durum", "utl_prog": "ilerleme", // kısa tutun: "utl_404": "404", "utl_err": "HATA", "utl_oserr": "OS-hatası", "utl_found": "bulundu", "utl_defer": "ertele", "utl_yolo": "YOLO", "utl_done": "tamamlandı", "ul_flagblk": "dosyalar sıraya alındı
          ancak başka bir tarayıcı sekmesinde meşgul bir up2k var,
          bu nedenle önce onun bitmesini bekliyor", "ul_btnlk": "sunucu yapılandırması bu anahtarı bu duruma kilitledi", "udt_up": "Yükle", "udt_srch": "Ara", "udt_drop": "buraya bırakın", "u_nav_m": '
          Pekâlâ, bakalım neyin varmnış?
          Enter = Dosyalar (bir veya birden fazla)\nESC = Bir klasör (alt klasörler dahil)', "u_nav_b": 'DosyalarTek klasör', "cl_opts": "aç / kapat", "cl_hfsz": "dosya boyutu", //m "cl_themes": "tema", "cl_langs": "dil", "cl_ziptype": "klasör indirme", "cl_uopts": "up2k aç / kapat", "cl_favico": "favicon", "cl_bigdir": "big dirs", "cl_hsort": "#sort", "cl_keytype": "anahtar notasyonu", "cl_hiddenc": "gizli sütunlar", "cl_hidec": "gizle", "cl_reset": "sıfırla", "cl_hpick": "aşağıdaki tabloda gizlemek için sütun başlıklarına dokunun", "cl_hcancel": "sütun gizleme iptal edildi", "cl_rcm": "sağ tık menüsü", //m "ct_grid": '田 ızgara', "ct_ttips": '◔ ◡ ◔">ℹ️ ipuçları', "ct_thumb": 'ızgara görünümünde, simgeler ve küçük resimler arasında geçiş yapın$NKısayol: T">🖼️ küçük resimler', "ct_csel": 'ızgara görünümünde dosya seçimi için CTRL ve SHIFT tuşlarını kullanın">seç', "ct_dsel": 'ızgara görünümünde sürükleyerek seçimi kullanın">sürükle', //m "ct_dl": 'dosyaya tıklandığında indirmeyi zorla (satır içinde görüntüleme)">dl', //m "ct_ihop": 'resim görüntüleyici kapatıldığında, en son görüntülenen dosyaya kaydırın">g⮯', "ct_dots": 'gizli dosyaları göster (sunucu izin veriyorsa)">nokta dosyaları', "ct_qdel": 'dosyaları silerken yalnız bir kez onay isteyin">qdel', "ct_dir1st": 'sıralamada klasörleri dosyalardan önceye koy">ilk 📁', "ct_nsort": 'doğal sıralama (başında sayı bulunan isimler için)">dsort', "ct_utc": 'tüm zaman damgalarını UTC diliminde göster">UTC', "ct_readme": 'README.md\'yi klasör listelemelerinde göster">📜 readme', "ct_idxh": 'index.html\'i klasör listelemeleri yerine göster">htm', "ct_sbars": 'kaydırma çubuklarını göster">⟊', "cut_umod": "eğer bir dosya sunucuda mevcutsa sunucudaki dosyanın son-değiştirilme zaman damgasını yereldekiyle değiştir (yazma+silme izinlerini gerektirir)\">re📅", "cut_turbo": "yolo butonu, bunu muhtemelen etkinleştirmek itemezsiniz:$N$Nyalnızca aynı anda çok fazla sayıda dosya yüklerken harhangi bir sebepten yüklemeyi durdurup anında yeniden başlatmanız gerekirse kullanın$N$Nbu, hash fonksiyonunu basit bir ile değiştirir"sunucudaki boyutu da aynı mı?" dosya içerikleri farklıysa yükleme devam ETMEYECEKTİR!$N$Nyükleme tamamlandığında bu ayarı kapatın ve sonra aynı dosyaları yeniden "yüklemeyi" deneyin ki sunucu bu dosyaları doğrulayabilsin\">turbo", "cut_datechk": "turbo modu açıksa hiçbir etkisi yoktur$N$Nyolo faktörünü az bir miktar düşürür; sunucudaki dosya zaman damgalarının sizinkiyle eşleşip eşleşmediğini kontrol eder$N$Nteorik olarak tamamlanmamış / bozuk yüklemelerin çoğunu yakalaması gerekir ancak turbo devre dışı bırakıldıktan sonra yapılan doğrulama geçişinin yerini tutamaz\">date-chk", "cut_u2sz": "her yükleme parçasının boyutu (MiB cinsinden); büyük değerler Atlantik boyunca daha iyi iletilir. Çok güvenilmez bağlantılarda düşük değerler deneyin", "cut_flag": "aynı anda sadece bir sekmenin yükleme yapabilmesini sağlayın $N -- diğer sekmelerde de bu ayar açık olmalıdır $N -- sadece aynı alan adı altındaki sekmeleri etkiler", "cut_az": "dosyaları en küçüğünden başlamak yerine alfabetik sırayla yükleyin$N$Nalefabetik sıralama, sunucuda bir şeylerin yanlış gidip gitmediğini gözlemlemenizi daha kolay hale getirebilir ancak fiber / LAN üzerinde yüklemeyi biraz yavaşlatır", "cut_nag": "yükleme tamamlandığında işletim sistemi bildirimi$N(sadece tarayıcı veya serkme aktif değilse)", "cut_sfx": "yükleme tamamlandığında sesli bildirim$N(sadece tarayıcı veya serkme aktif değilse)", "cut_mt": "dosya hash'lemesini hızlandırmak için çoklu izlekleri kullan$N$Nbu ayar web işlerini kullanır ve daha fazla$Nmore RAM (fazladan 512 MiB'a kadar) kullanır$N$https bağlantısını %30, http'yi ise 4.5 kat hızlandırır\">mt", "cut_wasm": "tarayıcının varsayılan hash fonksiyonu yerine WASM'ı kullan; Chrome tabanlı tarayıcılarda performansla birlikte CPU kullanımını artırır, ayrıca Chrome'un birçok eski sürümünde bu ayar etkinleştiğinde tarayıcının tüm RAM'i yemesine ve çökmesine sebep olan hatalar bulunur\">wasm", "cft_text": "favicon yazısı (devre dışı bırakmak için boş bırakıp yenileyin)", "cft_fg": "ön plan rengi", "cft_bg": "arka plan rengi", "cdt_lim": "bir klasörde gösterilecek maksimum dosya sayısı", "cdt_ask": "aşağı kaydırırken,$Ndaha fazla dosya yüklemek yerine,$Nne yapılacağını sor", "cdt_hsort": "`medya-URL'lerinde dahil edilecek sıralama kurallarının sayısı (`,sorthref`). Bunu 0 olarak ayarlamak, tıklanırken medya bağlantılarına dahil edilen sıralama kurallarını da yok sayacaktır", "cdt_ren": "özel sağ tık menüsünü etkinleştir, normal menü shift + sağ tık ile erişilebilir\">etkinleştir", //m "cdt_rdb": "özel menü zaten açıkken tekrar sağ tıklanınca normal sağ tık menüsünü göster\">x2", //m "tt_entree": "navigasyon panosunu göster (yan dizin panosu)$NHotkey: B", "tt_detree": "içerik haritasını göster$Kısayol: B", "tt_visdir": "seçili klasöre kaydır", "tt_ftree": "klasör ağacını / metin dosyalarını aç/kapat$Kısayol: V", "tt_pdock": "üstteki bir pencerede üst klasörleri göster", "tt_dynt": "ağaç genişledikçe otomatik büyüt", "tt_wrap": "kelime sarma", "tt_hover": "fare ile üzerine gelindiğinde taşan satırları göster$N( fare imleci sol kenarda değilse kaydırmayı bozar )", "ml_pmode": "klasör sonunda...", "ml_btns": "komutlar", "ml_tcode": "dönüştür", "ml_tcode2": "dönüştür", "ml_tint": "tonlama", "ml_eq": "ses eşitleyici", "ml_drc": "dinamik aralık sıkıştırıcı", "ml_ss": "sessizliği atla", //m "mt_loop": "bir şarkıyı döngüye al / tekrar et\">🔁", "mt_one": "bir şarkıdan sonra dur\">1️⃣", "mt_shuf": "klasörlerdeki şarkıları karıştır\">🔀", "mt_aplay": "sunucuya erişmek için kullandığın bağlantıda geçerli bir şarkı varsa otomatik oynat$N$Nbunu etkisiz kılmak aynı zamanda müzik oynatıldığında sayfa URL'nin değişmesini de engeller\">a▶", "mt_preload": "aralıksız oynatma için sıradaki şarkıyı önceden yüklemeye başla\">ön yükleme", "mt_prescan": "son şarkı bitmeden önce bir sonraki klasöre git$Nweb tarayıcısını mutlu tutar$Nbu nedenle oynatmayı durdurmaz\">nav", "mt_fullpre": "tüm şarkıyı ön yüklemeye çalış;$N✅ güvenilmez bağlantılarda etkinleştir,$N❌ yavaş bağlantılarda devre dışı bırak\">full", "mt_fau": "telefonlarda, bir sonraki şarkı yeterince hızlı ön yüklenmezse müziğin durmasını önle (etiketlerin bozuk görünmesine neden olabilir)\">☕️", "mt_waves": "dalga formu kaydırıcı:$Nses genliğini kaydırıcıda göster\">~s", "mt_npclip": "şu anda çalan şarkıyı kopyalamak için düğmeleri göster\">/np", "mt_m3u_c": "seçilen şarkıları m3u8 çalma listesi girişleri olarak kopyalamak için düğmeleri göster\">📻", "mt_octl": "os entegrasyonu (medya kısayolları / osd)\">os-ctl", "mt_oseek": "OS entegrasyonuyla şarkı ilerletmeye izin ver$N$Nnot: bazı cihazlarda (iPhone'lar)$Nbu, sıradaki şarkı düğmesini değiştirir\">ilerletme", "mt_oscv": "ekranda albüm kapaklarını göster\">görsel", "mt_follow": "oynatılan müzik ibaresini görünümde tut\">🎯", "mt_compact": "kompakt kontroller\">⟎", "mt_uncache": "önbelleği temizle  (bunu, tarayıcınızın bozuk bir şarkı kopyasını önbelleğe alması nedeniyle çalmayı reddettiğinde deneyin)\">önbelleği temizle", "mt_mloop": "açık klasörü döngüye al\">🔁 döngü", "mt_mnext": "bir sonraki klasörü yükle ve devam et\">📂 sonraki", "mt_mstop": "oynatmayı durdur\">⏸ durdur", "mt_cflac": "flac / wav'ı {0}'a dönüştür\">flac", "mt_caac": "aac / m4a'yı {0}'a dönüştür\">aac", "mt_coth": "diğer tüm formatları (mp3 hariç) {0}'a dönüştür\">oth", "mt_c2opus": "masaüstü, dizüstü bilgisayarlar, android için en iyi seçim\">opus", "mt_c2owa": "opus-weba, iOS 17.5 ve üzeri için\">owa", "mt_c2caf": "opus-caf, iOS 11 ile 17 arasında için\">caf", "mt_c2mp3": "çok eski cihazlarda bunu kullanın\">mp3", "mt_c2flac": "en iyi ses kalitesi, ancak büyük indirmeler\">flac", "mt_c2wav": "sıkıştırılmamış oynatma (daha da büyük)\">wav", "mt_c2ok": "güzel, iyi seçim", "mt_c2nd": "bu, cihazınız için önerilen çıkış formatı değil, ama sorun değil", "mt_c2ng": "cihazınız bu çıkış formatını desteklemiyor gibi görünüyor, ama yine de deneyelim", "mt_xowa": "iOS'ta bu formatta arka plan oynatımını engelleyen hatalar var; lütfen bunun yerine caf veya mp3 kullanın", "mt_tint": "seekbar'da arka plan seviyesi (0-100)$ön belleğin daha az dikkat dağıtıcı olmasını sağlar", "mt_eq": "`ekolayzer ve kazanç kontrolünü aktifleştirir;$N$Nboost `0` = standart %100 ses (varsayılan)$N$Ngenişlik `1  ` = standart çift kanal (varsayılan)$Ngenişlik `0.5` = %50 sol-sağ crossfeed$Ngenişlik `0  ` = mono$N$Nartış `-0.8` & artış `10` = vokal kaldırma :^)$N$Nekolayzeri aktifleştirmek, aralıksız albümleri gerçekten aralıksız yapar, bu yüzden tüm değerleri 0'da bırakın (width = 1 hariç) tabii bunu umursuyorsnaız", "mt_drc": "dinamik aralık sıkıştırıcıyı (ses düzleştiriciyi) aktifleştirir; spagettiyi dengelemek için aynı zamanda EQ'yu da açar, bunun olmasını istemiyorsanız EQ'daki 'genişlik' hariç tüm alanları 0 yapın$N$Nsınır dB'inin üstündeki tüm sesleri kısar; sınırı geçen her 1 dB için ancak 1 dB ses verilir, yani varsayılan eşik -24 ve oran 12 değerleri sesin -22 dB'den yükseğe çıkmayacağını ve EQ artışının 0,8'e, hatta ATK 0 ve hayvan gibi RLS (örneğin 90, bu sadece Firefox'ta çalışır; diğer tarayıcılarda RLS en fazla 1'dir) gibi değerlerle 1.,8'e kadar güvenle çıkabileceğini gösteriyor$N$N(git vikipediye bak, orada daha iyi açıklanıyor)", "mt_ss": "`sessizlik atlamayı etkinleştirir; ses `ses` altındayken ve konum ilk `bas`% veya son `son`% içindeyken başlangıç/bitiş yakınında oynatma hızını `ileri` ile çarpar", //m "mt_ssvt": "ses eşiği (0-255)\">ses", //m "mt_ssts": "etkin eşik (% parça, başlangıç)\">bas", //m "mt_sste": "etkin eşik (% parça, bitiş)\">son", //m "mt_sssm": "oynatma hızı çarpanı\">ileri", //m "mb_play": "oynat", "mm_hashplay": "bu ses dosyası oynatılsın mı?", "mm_m3u": "Oynatmak için Enter/Tamam tuşuna basın\nDüzenlemek için ESC/İptal tuşuna basın", "mp_breq": "firefox 82+ veya chrome 73+ veya iOS 15+ gerektirir", "mm_bload": "şu anda yükleniyor...", "mm_bconv": "{0} formatına dönüştürülüyor, lütfen bekleyin...", "mm_opusen": "tarayıcınız aac / m4a dosyalarını oynatamıyor;\nopus'a dönüştürme etkinleştirildi", "mm_playerr": "oynatma hatası: ", "mm_eabrt": "Oynatma denemesi iptal edildi", "mm_enet": "İnternet bağlantınızda bir bokluk var", "mm_edec": "Bu dosya muhtemelen bozuk!", "mm_esupp": "Tarayıcınız salak, bu ses formatını anlamıyor", "mm_eunk": "Bilinmeyen Hata", "mm_e404": "Ses oynatılamadı; hata 404: Dosya bulunamadı.", "mm_e403": "Ses oynatılamadı; hata 403: Erişim reddedildi.\n\nYeniden yüklemek için F5 tuşuna basın, oturumunuz kapanmış olabilir.", "mm_e415": "Ses oynatılamadı; hata 415: Dosya dönüştürme başarısız oldu; sunucu günlüklerini kontrol edin.", //m "mm_e500": "Ses oynatılamadı; hata 500: Sunucu günlüklerini kontrol edin.", "mm_e5xx": "Ses oynatılamadı; sunucu hatası ", "mm_nof": "yakınlarda başka ses dosyası bulunamadı", "mm_prescan": "Sonraki şarkı aranıyor...", "mm_scank": "Sonraki şarkı bulundu:", "mm_uncache": "önbellek temizlendi; tüm şarkılar bir sonraki çalınmada yeniden indirilecek", "mm_hnf": "bu şarkı artık mevcut değil", "im_hnf": "bu resim artık mevcut değil", "f_empty": 'bu klasör boş', "f_chide": 'bu, «{0}» sütununu gizleyecektir\n\nsütunları ayarlar sekmesinden yeniden açabilirsiniz', "f_bigtxt": "bu dosya {0} MiB boyutunda -- Cidden saf yazı olarak mı görüntülemek istiyo'n?", "f_bigtxt2": "dosyanın sadece sonunu görüntülemek ister misin? bu, takip etme/sonlandırma işlemini de etkinleştirecek ve gerçek zamanlı olarak yeni eklenen metin satırlarını gösterecektir.", "fbd_more": '
          {1} dosyadan {0} tanesi gösteriliyor; {2} tanesini ya da tümünü göster
          ', "fbd_all": '
          {1} dosyadan {0} tanesi gösteriliyor; tümünü göster
          ', "f_anota": "{1} dosyadan sadece {0} tanesi seçildi;\nTüm klasörü seçmek için önce en alta kaydırın.", "f_dls": 'bu klasördeki dosya linkleri\nindirme linklerine dönüştürüldü', "f_dl_nd": 'klasör atlanıyor (bunun yerine zip/tar indirmesini kullanın):\n', //m "f_partial": "Mevcutta yüklenen bir dosyayı güvenli bir şekilde indirmek için lütfen aynı adlı ama .PARTIAL uzantısına sahip olmayan dosyaya tıklayın. Lütfen bunu yapmak için İPTAL veya Esc tuşuna basın.\n\nTamam / Enter tuşuna basmak, bu uyarıyı yok sayacak ve bunun yerine .PARTIAL geçici dosyasını indirmeye devam edecektir ki bu da elinize bozuk veriler sunacaktır.", "ft_paste": "{0} ögeyi yapıştır$Kısayol: ctrl-V", "fr_eperm": 'yeniden adlandırılamıyor:\nbu klasörü “taşıma” izniniz yok', "fd_eperm": 'silinemiyor:\nbu klasörde “silme” izniniz yok', "fc_eperm": 'kesilemiyor:\nbu klasörde “taşıma“ izniniz yok', "fp_eperm": 'yapıştırılamıyor:\nbu klasörde “yazma“ izniniz yok', "fr_emore": "yeniden adlandırmak için en az bir öge seçin", "fd_emore": "silmek için en az bir öge seçin", "fc_emore": "kesmek için en az bir öge seçin", "fcp_emore": "panoya kopyalamak için en az bir öge seçin", "fs_sc": "bulunduğunuz klasörü paylaşın", "fs_ss": "seçilen dosyaları paylaşın", "fs_just1d": "birden fazla klasör seçemezsiniz\nveya bir seçimde dosyaları ve klasörleri karıştıramazsınız", "fs_abrt": "❌ iptal", "fs_rand": "🎲 rastgele.ad", "fs_go": "✅ paylaşımı oluştur", "fs_name": "isim", "fs_src": "kaynak", "fs_pwd": "şifre", "fs_exp": "son kullanma", "fs_tmin": "dakika", "fs_thrs": "saat", "fs_tdays": "gün", "fs_never": "sonsuz", "fs_pname": "isteğe bağlı bağlantı adı; boşsa rastgele olacaktır", "fs_tsrc": "paylaşılacak dosya veya klasör", "fs_ppwd": "isteğe bağlı şifre", "fs_w8": "paylaşım oluşturuluyor...", "fs_ok": "panoya kopyalamak için Enter/Tamam tuşuna basın\nkapatmak için ESC/İptal tuşuna basın", "frt_dec": "bazı bozuk dosya adlarını düzeltebilir\">url-decode", "frt_rst": "değiştirilen dosya adlarını orijinal haline döndür\">↺ sıfırla", "frt_abrt": "iptal et ve bu pencereyi kapat\">❌ iptal", "frb_apply": "YENİ ADI UYGULA", "fr_adv": "toplu / meta veri / desen yeniden adlandırma\">gelişmiş", "fr_case": "büyük/küçük harf duyarlı regex\">büyük/küçük harf", "fr_win": "windows'a uygun adlar; <>:"\\|?* karakterlerini Japonca tam genişlik karakterleriyle değiştir\">win", "fr_slash": "/ karakterini yeni klasörlerin oluşturulmasına neden olmayan bir karakterle değiştir\">/ yok", "fr_re": "`orijinal dosya adlarına uygulanacak regex arama deseni; yakalama grupları aşağıdaki format alanında `(1)` ve `(2)` gibi belirtilebilir", "fr_fmt": "`foobar2000'den esinlenilmiştir:$N`(title)` şarkı başlığıyla değiştirilir,$N`[(artist) - ](title)` sanatçı boşsa [bu] kısmı atlar$N`$lpad((tn),2,0)` parça numarasını 2 basamağa doldurur", "fr_pdel": "sil", "fr_pnew": "farklı kaydet", "fr_pname": "yeni ayarınızı adlandırın", "fr_aborted": "iptal edildi", "fr_lold": "eski ad", "fr_lnew": "yeni ad", "fr_tags": "seçilen dosyalar için etiketler (salt okunur, sadece referans için):", "fr_busy": "{0} öğe yeniden adlandırılıyor...\n\n{1}", "fr_efail": "yeniden adlandırma başarısız:\n", "fr_nchg": "{0} yeni adlardan bazıları win ve/veya / yok nedeniyle değiştirildi\n\nBu değiştirilmiş yeni adlarla devam etmek için Tamam'a basın.", "fd_ok": "silme tamam", "fd_err": "silme başarısız:\n", "fd_none": "hiçbir şey silinmedi; belki sunucu yapılandırması (xbd) tarafından engellenmiştir?", "fd_busy": "{0} öğe siliniyor...\n\n{1}", "fd_warn1": "Bu {0} öge silinsin mi?", "fd_warn2": "Son şans! Geri alma imkanın yok. Silinsin mi?", "fc_ok": "{0} öge kesildi", "fc_warn": '{0} öge kesildi\n\namma velakin: sadece bu tarayıcı penceresine yapıştırılabilirler\n(çünkü seçiminin boyutu hayvan gibi)', "fcc_ok": "{0} öge panoya kopyalandı", "fcc_warn": '{0} öge panoya kopyalandı\n\namma velakin: sadece bu tarayıcı penceresine yapıştırılabilirler\n(çünkü seçiminin boyutu hayvan gibi)', "fp_apply": "bu adları kullan", "fp_skip": "çakışmaları atla", //m "fp_ecut": "önce bazı dosyaları / klasörleri kes veya kopyala, sonra yapıştır / taşı\n\nnot: farklı tarayıcı sekmeleri arasında kes / yapıştır yapabilirsiniz", "fp_ename": "{0} öğe buraya taşınamaz çünkü adlar zaten alınmış. Devam etmek için aşağıda yeni adlar verin veya atlamak için adları boş bırakın (\"çakışmaları atla\"):", //m "fcp_ename": "{0} öğe buraya kopyalanamaz çünkü adlar zaten alınmış. Devam etmek için aşağıda yeni adlar verin veya atlamak için adları boş bırakın (\"çakışmaları atla\"):", //m "fp_emore": "hâlâ düzeltilmesi gereken bazı dosya adı çakışmaları var", "fp_ok": "taşıma tamam", "fcp_ok": "kopyalama tamam", "fp_busy": "{0} öğe taşınıyor...\n\n{1}", "fcp_busy": "{0} öğe kopyalanıyor...\n\n{1}", "fp_abrt": "İptal ediliyor...", "fp_err": "taşıma başarısız:\n", "fcp_err": "kopyalama başarısız:\n", "fp_confirm": "bu {0} öğeyi buraya taşımak istiyor musunuz?", "fcp_confirm": "bu {0} öğeyi buraya kopyalamak istiyor musunuz?", "fp_etab": 'diğer tarayıcı sekmesinden pano okunamadı', "fp_name": "cihazınızdan bir dosya yüklüyorsunuz. Bir ad verin:", "fp_both_m": '
          yapıştırılacak şeyi seçin
          Enter = «{1}» içinden {0} dosyayı taşı\nESC = cihazınızdan {2} dosyayı yükle', "fcp_both_m": '
          yapıştırılacak şeyi seçin
          Enter = «{1}» içinden {0} dosyayı kopyala\nESC = cihazınızdan {2} dosyayı yükle', "fp_both_b": 'TaşıYükle', "fcp_both_b": 'KopyalaYükle', "mk_noname": "bunu yapmadan önce soldaki boşluğa bir şeyler yazsana :p", "nmd_i1": "ayrıca istediğin dosya uzantısını ekleyebilirsin, örneğin .md", //m "nmd_i2": "silme iznin olmadığı için yalnızca .{0} dosyaları oluşturabilirsin", //m "tv_load": "Metin belgesi yükleniyor:\n\n{0}\n\n{1}% ({2} of {3} MiB yüklendi)", "tv_xe1": "metin dosyası yüklenemedi:\n\nhata ", "tv_xe2": "404, dosya bulunamadı", "tv_lst": "metin dosyalarının listesi", "tvt_close": "klasör görünümüne dön$NKısayol: M (veya Esc)\">❌ kapat", "tvt_dl": "bu dosyayı indir$NKısayol: Y\">💾 indir", "tvt_prev": "önceki belgeyi göster$NKısayol: i\">⬆ önceki", "tvt_next": "sonraki belgeyi göster$NKısayol: K\">⬇ sonraki", "tvt_sel": "dosyayı seç$NKısayol: S\">seç", "tvt_j": "json güzelleştir$NKısayol: shift-J\">j", //m "tvt_edit": "dosyayı metin düzenleyicisinde aç$NKısayol: E\">✏️ düzenle", "tvt_tail": "dosyalardaki değişiklikleri izle; yeni satırları gerçek zamanlı göster\">📡 takip", "tvt_wrap": "kelime sarma\">↵", "tvt_atail": "sayfanın altına sabitle\">⚓", "tvt_ctail": "terminal renklerini çöz (ansi kaçış kodları)\">🌈", "tvt_ntail": "önbellek limiti (kaç byte yüklenmiş metin tutulacağı)", "m3u_add1": "şarkı m3u çalma listesine eklendi", "m3u_addn": "{0} şarkı m3u çalma listesine eklendi", "m3u_clip": "m3u çalma listesi şimdi panoya kopyalandı\n\nyeni bir metin dosyası oluşturmalı ve adını dosya.m3u koymalısınız ve çalma listesini o belgeye yapıştırmalısınız; bu sayede oynatılabilir hale gelecektir", "gt_vau": "videoları gösterme, sadece sesi oynat\">🎧", "gt_msel": "dosya seçimlerini etkinleştir; bir dosyayı geçersiz kılmak için ctrl-tıkla$N$N<em>etkin olduğunda: bir dosyayı / klasörü açmak için çift tıkla</em>$N$NKısayol: S\">çoklu seçim", "gt_crop": "küçültülmüş önizlemeleri ortala\">kırp", "gt_3x": "yüksek çözünürlüklü önizlemeler\">3x", "gt_zoom": "yakınlaştır", "gt_chop": "kes", "gt_sort": "sırala", "gt_name": "isim", "gt_sz": "boyut", "gt_ts": "tarih", "gt_ext": "tip", "gt_c1": "dosya adlarını daha fazla kısalt (daha az göster)", "gt_c2": "dosya adlarını daha az kısalt (daha fazla göster)", "sm_w8": "aranıyor...", "sm_prev": "aşağıdaki arama sonuçları önceki bir sorgudan alınmıştır:\n ", "sl_close": "arama sonuçlarını kapat", "sl_hits": "gösterilen {0} sonuç", "sl_moar": "daha fazla yükle", "s_sz": "boyut", "s_dt": "tarih", "s_rd": "yol", "s_fn": "isim", "s_ta": "etiketler", "s_ua": "yükleme@", "s_ad": "gel.", "s_s1": "en az MiB", "s_s2": "en fazla MiB", "s_d1": "en az iso8601", "s_d2": "en fazla. iso8601", "s_u1": "yüklendi", "s_u2": "veya önce", "s_r1": "dosya yolu   içerir; (boşlukla ayrılmış)", "s_f1": "isim   içerir; (-nope kullan)", "s_t1": "etiketler   içerir; (^=baş, son=$)", "s_a1": "özel metadata özellikleri", "md_eshow": "görüntülenemiyor: ", "md_off": "[📜beni-oku], [⚙️] içinde kapalıdır -- belge gizli", "badreply": "Sunucudan gelen yanıt çözümlenemedi", "xhr403": "403: Erişim reddedildi\n\nF5'e basmayı deneyin, oturumunuz kapanmış olabilir", "xhr0": "bilinmiyor (muhtemelen sunucuya bağlantı kaybedildi veya sunucu çevrimdışı)", "cf_ok": "pardon yahu -- DD" + wah + "oS koruması şey etti\n\n30 saniyeye düzelmiş olurum\n\nbi' halt olmazsa f5'e basarak sayfayı yenile", "tl_xe1": "alt klasörler listelenemiyor:\n\nhata ", "tl_xe2": "404: klasör bulunamadı", "fl_xe1": "dosyalar listelenemiyor:\n\nhata ", "fl_xe2": "404: klasör bulunamadı", "fd_xe1": "alt klasör oluşturulamadı:\n\nhata ", "fd_xe2": "404: Üst klasör bulunamadı", "fsm_xe1": "mesaj gönderilemedi:\n\nhata ", "fsm_xe2": "404: Üst klasör bulunamadı", "fu_xe1": "sunucudan unpost(?) listesini yüklemek başarısız oldu:\n\nhata ", "fu_xe2": "404: Dosya bulunamadı!!", "fz_tar": "sıkıştırılmamış gnu-tar dosyası (linux / mac)", "fz_pax": "sıkıştırılmamış pax-format tar (daha yavaş)", "fz_targz": "gnu-tar ile gzip seviye 3 sıkıştırma$N$Nbu genellikle çok yavaştır, bu yüzden$Nsıkıştırılmamış tar kullanın", "fz_tarxz": "gnu-tar ile xz seviye 1 sıkıştırma$N$Nbu genellikle çok yavaştır, bu yüzden$Nsıkıştırılmamış tar kullanın", "fz_zip8": "utf8 dosya adları ile zip (windows 7 ve daha eski sürümlerde sorunlu olabilir)", "fz_zipd": "gerçekten eski yazılımlar için geleneksel cp437 dosya adları ile zip", "fz_zipc": "erken hesaplanan crc32 ile cp437,$NMS-DOS PKZIP v2.04g (ekim 1993) için$N(indirmenin başlamasından önce işlenmesi daha uzun sürer)", "un_m1": "aşağıdan son yüklemelerinizi (veya tamamlanmamış olanları) silebilirsiniz", "un_upd": "yenile", "un_m4": "veya aşağıda görünen dosyaları paylaşın:", "un_ulist": "göster", "un_ucopy": "kopyala", "un_flt": "isteğe bağlı filtre:  URL şunu içermelidir:", "un_fclr": "filtreyi temizle", "un_derr": 'unpost-delete failed:\n', "un_f5": 'bir şeyler bozuldu, lütfen yenilemeyi deneyin veya F5 tuşuna basın', "un_uf5": "üzgünüm ama bu yükleme iptal edilmeden önce sayfayı yenilemeniz gerekiyor (F5 veya CTRL-R tuşlarına basarak)", "un_nou": 'uyarı: sunucu tamamlanmamış yüklemeleri göstermek için çok meşgul; birazdan "yenile" bağlantısına tıklayın', "un_noc": 'uyarı: tamamen yüklenmiş dosyaların unpost edilmesi sunucu yapılandırmasında etkinleştirilmemiştir/izin verilmemiştir', "un_max": "ilk 2000 dosya gösteriliyor (filtreyi kullan)", "un_avail": "son yüklemelerin {0} tanesi silinebilir
          tamamlanmayanların {1} tanesi iptal edilebilir", "un_m2": "yükleme zamanına göre sıralandı; en son önce:", "un_no1": "Hayda! yeterince güncel yükleme yok", "un_no2": "Hayda! o filtreye uyan yeterince güncel yükleme yok", "un_next": "aşağıdaki {0} dosyayı sil", "un_abrt": "iptal", "un_del": "sil", "un_m3": "son yüklemelerinizi hazırlanıyor...", "un_busy": "{0} dosya siliniyor...", "un_clip": "{0} bağlantı panoya kopyalandı", "u_https1": "daha iyi performans", "u_https2": "için https'i", "u_https3": "kullanın", "u_ancient": 'tarayıcınız resmen fosilleşmiş -- belki de bup kullanmalısınız', "u_nowork": "firefox 53+ veya chrome 57+ veya iOS 11+ gerekiyor", "tail_2old": "firefox 105+ veya chrome 71+ veya iOS 14.5+ gerekiyor", "u_nodrop": 'tarayıcınız sürükleyip bırakmak için çok eski', "u_notdir": "bu bir klasör değil!\n\ntarayıcınız çok eski,\nlütfen bunun yerine sürükleyip bırakmayı deneyin", "u_uri": "başka tarayıcı pencerelerinden sürükle-bırak ile yükleme yapmak için,\nlütfen büyük yükleme düğmesinin üstüne bırakın", "u_enpot": 'Patates arayüze geçiş yap (yükleme hızını artırabilir)', "u_depot": 'Havalı arayüze geçiş yap (yükleme hızını azaltabilir)', "u_gotpot": 'yükleme hızını artırmak için patates arayüzüne geçiliyor,\n\nkatılmıyorsanız geri dönmekte özgürsünüz!', "u_pott": "

          dosyalar:   {0} tamamlandı,   {1} başarısız,   {2} meşgul,   {3} sırada bekliyor

          ", "u_ever": "bu temel yükleyici; up2k en az
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1 gerektirir", "u_su2k": 'bu temel yükleyici; up2k daha iyidir', "u_uput": 'hız için optimize et (kontrol toplamını atla)', "u_ewrite": 'bu klasöre yazma izniniz yok', "u_eread": 'bu klasörü okuma izniniz yok', "u_enoi": 'dosya arama sunucu yapılandırmasında etkin değil', "u_enoow": "üstüne yazma burada çalışmayacak; Silme izni gerekiyor", "u_badf": 'Bu {0} dosya (toplam {1} arasında) atlandı, muhtemelen dosya sistemi izinleri nedeniyle:\n\n', "u_blankf": 'Bu {0} dosya (toplam {1} arasında) boş / boş; yine de yüklemek ister misiniz?\n\n', "u_applef": 'Bu {0} dosya (toplam {1} arasında) muhtemelen istenmiyor;\nAşağıdaki dosyaları ATLAMAK için TAMAM/Enter tuşuna basın,\nyüklemek için İptal/ESC tuşuna basın\n\n', "u_just1": '\nBelki sadece bir dosya seçerseniz daha iyi çalışır', "u_ff_many": "eğer Linux / MacOS / Android kullanıyorsanız bu kadar çok dosya yüklemenin Firefox'u çökertme ihtimali var!\nEğer çökerse lütfen tekrar deneyin (veya Chrome kullanın).", "u_up_life": "Bu yükleme, tamamlandıktan {0} süre sonra sunucudan silinecek\n", "u_asku": 'bu {0} dosya {1} içerisine yükleniyor', "u_unpt": "bu yüklemeyi sol üstteki 🧯 ile geri alabilir / silebilirsiniz", "u_bigtab": '{0} dosya görüntülenmek üzere\n\nBu kadar fazlası tarayıcınızı çökertebilir, emin misiniz?', "u_scan": 'Dsyalar tarıyor...', "u_dirstuck": 'dizin yineleyicisi aşağıdaki {0} öğeye erişmeye çalışırken takıldı; atlanacak:', "u_etadone": 'Tamamlandı ({0}, {1} dosya)', "u_etaprep": '(yüklemeye hazırlanıyor)', "u_hashdone": 'hash\'leme tamamlandı', "u_hashing": 'hash\'leniyor', "u_hs": 'el sıkılıyor...', "u_started": "dosyalar şu anda yükleniyor; [🚀]'i gör", "u_dupdefer": "başka bir dosyanın kopyası; diğer tüm dosyalardan sonra işlenecek", "u_actx": "diğer pencereler/sekmeler arasında geçiş yaparken
          performansın düşmemesi için bu yazıya tıklayın", "u_fixed": "Timam!  Hallettim 👍", "u_cuerr": "parça {0} / {1} yüklenemedi;\nmuhtemelen zararsız, devam ediliyor\n\ndosya: {2}", "u_cuerr2": "sunucu yüklemeyi reddetti (parça {0} / {1});\nsonra tekrar denenecek\n\ndosya: {2}\n\nhata ", "u_ehstmp": "tekrar denenecek; sağ alt köşeye mak", "u_ehsfin": "sunucu yüklemeyi tamamlama isteğini reddetti; tekrar deniyor...", "u_ehssrch": "sunucu arama yapma isteğini reddetti; tekrar deniyor...", "u_ehsinit": "sunucu yüklemeyi başlatma isteğini reddetti; tekrar deniyor...", "u_eneths": "yükleme el sıkışması sırasında ağ hatası; tekrar deniyor...", "u_enethd": "hedef varlığını test ederken ağ hatası; tekrar deniyor...", "u_cbusy": "ağ kesintisinden sonra sunucunun bize tekrar güvenmesini bekliyoruz...", "u_ehsdf": "sunucuda disk alanı kalmadı!\n\ndevam edebilmek için birinin\nyeterli alan açmasını bekleyeceğiz", "u_emtleak1": "görünüşe göre web tarayıcınızda bir bellek sızıntısı var;\nlütfen", "u_emtleak2": ' https\'ye geçin (önerilir) veya ', "u_emtleak3": ' ', "u_emtleakc": 'sırayla şunları deneyin:\n
          • sayfayı yenilemek için F5\'e basın
          • sonra  mt  düğmesini  ⚙️ ayarlar menüsünde devre dışı bırakın
          • ve o yüklemeyi tekrar deneyin
          Yüklemeler biraz daha yavaş olacaktır, ama salla gitsin.\nSorun için özür dileriz!\n\nDipnot: chrome v107 için bir hata düzeltmesi', "u_emtleakf": 'şunları deneyin:\n
          • sayfayı yenilemek için F5\'e basın
          • sonra yükleme arayüzünde 🥔 (patates) seçeneğini etkinleştirin
          • ve yüklemeyi tekrar deneyin
          \nDipnot: firefox için muhtemel bir hata düzeltmesi', "u_s404": "sunucuda bulunamadı", "u_expl": "açıklama", "u_maxconn": "çoğu tarayıcı bunu 6 ile sınırlar, ancak firefox bunu about:config içinde connections-per-server ile artırmanıza izin verir", "u_tu": '

          UYARI: turbo etkin,  istemci tamamlanmamış yüklemeleri algılayamayabilir ve devam ettiremeyebilir; turbo düğmesinden ipuçlarına bakabilirsiniz

          ', "u_ts": '

          UYARI: turbo etkin,  arama sonuçları yanlış olabilir; turbo düğmesi ipuçlarına bakın

          ', "u_turbo_c": "turbo sunucu yapılandırmasında devre dışı bırakıldı", "u_turbo_g": "turbo devre dışı bırakılıyor çünkü bu alanda\ndizin listeleme ayrıcalıklarınız yok", "u_life_cfg": 'yüklemeleri dk (veya saat) sonra otomatik olarak sil', "u_life_est": 'yükleme --- sonra silinecek', "u_life_max": 'bu klasör bir\nmaksimum ömür {0} uygular', "u_unp_ok": 'unpost {0} için izin verildi', "u_unp_ng": 'unpost izin verilmeyecek', "ue_ro": 'bu klasöre erişiminiz Salt Okuma\n\n', "ue_nl": 'şu anda oturum açmamışsınız', "ue_la": 'şu anda "{0}" olarak oturum açmışsınız', "ue_sr": 'şu anda dosya arama modundasınız\n\nbüyüteç simgesine tıklayarak yükleme moduna geçin 🔎 (büyük ARAMA düğmesinin yanındaki) ve tekrar yüklemeyi deneyin\n\nyeter be', "ue_ta": 'tekrar yüklemeyi deneyin, artık çalışsana be', "ue_ab": "bu dosya zaten başka bir klasöre yükleniyor ve o yükleme tamamlanmadan dosya başka bir yere yüklenemez.\n\nİlk yüklemeyi iptal edip unutmak için sol üstteki 🧯 simgesini kullanabilirsiniz.", "ur_1uo": "OK: Dosya başarıyla yüklendi", "ur_auo": "OK: {0} dosyanın tamamı başarıyla yüklendi", "ur_1so": "OK: Dosya sunucuda bulundu", "ur_aso": "OK: {0} dosyanın tamamı sunucuda bulundu", "ur_1un": "Yükleme başarısız oldu, üzgünüm", "ur_aun": "{0} yüklemenin tamamı başarısız oldu, üzgünüm", "ur_1sn": "Dosya sunucuda bulunamadı", "ur_asn": "{0} dosyas sunucuda bulunamadı", "ur_um": "Tamamlandı;\n{0} yükleme başarılı,\n{1} yükleme başarısız oldu, üzgünüm", "ur_sm": "Tamamlandı;\n{0} dosya sunucuda bulundu,\n{1} dosya sunucuda bulunamadı", "rc_opn": "aç", //m "rc_ply": "oynat", //m "rc_pla": "ses olarak oynat", //m "rc_txt": "dosya görüntüleyicide aç", //m "rc_md": "metin düzenleyicide aç", //m "rc_dl": "i̇ndir", //m "rc_zip": "arşiv olarak indir", //m "rc_cpl": "bağlantıyı kopyala", //m "rc_del": "sil", //m "rc_cut": "kes", //m "rc_cpy": "kopyala", //m "rc_pst": "yapıştır", //m "rc_rnm": "yeniden adlandır", //m "rc_nfo": "yeni klasör", //m "rc_nfi": "yeni dosya", //m "rc_sal": "tümünü seç", //m "rc_sin": "seçimi tersine çevir", //m "rc_shf": "bu klasörü paylaş", //m "rc_shs": "seçimi paylaş", //m "lang_set": "Değişikliklerin etki göstermesi için sayfa yenilensin mi?", "splash": { "a1": "yenile", "b1": "N'aber aga   (giriş yapmamışsın)", "c1": "çıkış yap", "d1": "yığını yolla", "d2": "tüm aktif iş parçacıklarının durumunu gösterir", "e1": "cfg'yi yenile", "e2": "yapılandırma dosyalarını yenile (hesaplar/hacimler/hacim bayrakları),$Nve tüm e2ds hacimlerini yeniden tarayın$N$Nnot: global ayarlardaki herhangi bir değişiklik$Netkili hale gelmesi için tam bir yeniden başlatma gerektirir", "f1": "göz atabilirsiniz:", "g1": "yükleyebilirsiniz:", "cc1": "diğer şeyler:", "h1": "k304'ü devre dışı bırak", "i1": "k304'ü etkinleştir", "j1": "k304'ü etkinleştirmek, her HTTP 304'te istemcinizin bağlantısını keser, bu da bazı hatalı proxy'lerin takılmasını önleyebilir (sayfaların birdenbire yüklenmesinin durması gibi); ama bu, aynı zamanda genel olarak işleyişi yavaşlatır", "k1": "istemci ayarlarını sıfırla", "l1": "daha fazlası için giriş yap:", "m1": "hoş geldin,", "n1": "404 bulunamadı  ┐( ´ -`)┌", "o1": 'ya da erişim iznin yok -- bir şifre dene veya ana sayfaya dön', "p1": "403 yasaklandı  ~┻━┻", "q1": 'bir şifre kullan veya ana sayfaya dön', "r1": "ana sayfaya dön", ".s1": "yeniden tara", "t1": "işlem", "u2": "son sunucu yazma zamanı$N( yükleme / yeniden adlandırma / ... )$N$N17d = 17 gün$N1h23 = 1 saat 23 dakika$N4m56 = 4 dakika 56 saniye", "v1": "bağlan", "v2": "bu sunucuyu yerel HDD olarak kullan", "w1": "https'ye geç", "x1": "şifreyi değiştir", "y1": "paylaşılanları düzenle", "z1": "gizli paylaşımın kilidini aç:", "ta1": "ilk önce yeni şifreyi doldur", "ta2": "yeni şifreyi onaylamak için tekrar girin:", "ta3": "bir yazım hatası bulundu; lütfen tekrar deneyin", "nop": "HATA: Parola boş olamaz", //m "nou": "HATA: Kullanıcı adı ve/veya parola boş olamaz", //m "aa1": "gelen dosyalar:", "ab1": "no304'ü devre dışı bırak", "ac1": "no304'ü etkinleştir", "ad1": "no304'ü etkinleştirmek, tüm önbelleği devre dışı bırakır; bunu k304 yeterli olmadıysa deneyin. Bu, büyük miktarda ağ trafiği israf edecektir!", "ae1": "aktif indirmeler:", "af1": "son yüklemeleri göster", "ag1": "bilinen IdP kullanıcılarını göster", //m } }; ================================================ FILE: copyparty/web/tl/ukr.js ================================================ // Рядки, що закінчуються на //m, є неперевіреними машинними перекладами Ls.ukr = { "tt": "Українська", "cols": { "c": "кнопки дій", "dur": "тривалість", "q": "якість / бітрейт", "Ac": "аудіо кодек", "Vc": "відео кодек", "Fmt": "формат / контейнер", "Ahash": "контрольна сума аудіо", "Vhash": "контрольна сума відео", "Res": "роздільність", "T": "тип файлу", "aq": "якість аудіо / бітрейт", "vq": "якість відео / бітрейт", "pixfmt": "підвибірка / структура пікселів", "resw": "горизонтальна роздільність", "resh": "вертикальна роздільність", "chs": "аудіо канали", "hz": "частота дискретизації", }, "hks": [ [ "різне", ["ESC", "закрити різні речі"], "файловий менеджер", ["G", "перемкнути список / сітку"], ["T", "перемкнути мініатюри / іконки"], ["⇧ A/D", "розмір мініатюр"], ["ctrl-K", "видалити вибране"], ["ctrl-X", "вирізати до буфера"], ["ctrl-C", "копіювати до буфера"], ["ctrl-V", "вставити (перемістити/копіювати) сюди"], ["Y", "завантажити вибране"], ["F2", "перейменувати вибране"], "вибір файлів у списку", ["space", "перемкнути вибір файлу"], ["↑/↓", "перемістити курсор вибору"], ["ctrl ↑/↓", "перемістити курсор і вікно"], ["⇧ ↑/↓", "вибрати попередній/наступний файл"], ["ctrl-A", "вибрати всі файли / папки"], ], [ "навігація", ["B", "перемкнути хлібні крихти / панель навігації"], ["I/K", "попередня/наступна папка"], ["M", "батьківська папка (або згорнути поточну)"], ["V", "перемкнути папки / текстові файли в панелі навігації"], ["A/D", "розмір панелі навігації"], ], [ "аудіо плеєр", ["J/L", "попередня/наступна пісня"], ["U/O", "перемотати на 10сек назад/вперед"], ["0..9", "перейти до 0%..90%"], ["P", "відтворити/пауза (також запускає)"], ["S", "вибрати поточну пісню"], ["Y", "завантажити пісню"], ], [ "переглядач зображень", ["J/L, ←/→", "попереднє/наступне зображення"], ["Home/End", "перше/останнє зображення"], ["F", "повний екран"], ["R", "повернути за годинниковою стрілкою"], ["⇧ R", "повернути проти годинникової стрілки"], ["S", "вибрати зображення"], ["Y", "завантажити зображення"], ], [ "відео плеєр", ["U/O", "перемотати на 10сек назад/вперед"], ["P/K/Space", "відтворити/пауза"], ["C", "продовжити відтворення наступного"], ["V", "повтор"], ["M", "вимкнути звук"], ["[ and ]", "встановити інтервал повтору"], ], [ "переглядач текстових файлів", ["I/K", "попередній/наступний файл"], ["M", "закрити текстовий файл"], ["E", "редагувати текстовий файл"], ["S", "вибрати файл (для вирізання/копіювання/перейменування)"], ["Y", "завантажити текстовий файл"], //m ["⇧ J", "прикрасити json"], //m ] ], "m_ok": "Гаразд", "m_ng": "Скасувати", "enable": "Увімкнути", "danger": "НЕБЕЗПЕКА", "clipped": "скопійовано до буфера обміну", "ht_s1": "секунда", "ht_s2": "секунд", "ht_m1": "хвилина", "ht_m2": "хвилин", "ht_h1": "година", "ht_h2": "годин", "ht_d1": "день", "ht_d2": "днів", "ht_and": " і ", "goh": "панель керування", "gop": 'попередній сусід">назад', "gou": 'батьківська папка">вгору', "gon": 'наступна папка">далі', "logout": "Вийти ", "login": "увійти", //m "access": " доступ", "ot_close": "закрити підменю", "ot_search": "`пошук файлів за атрибутами, шляхом / іменем, музичними тегами, або будь-якою комбінацією$N$N`foo bar` = має містити «foo» і «bar»,$N`foo -bar` = має містити «foo», але не «bar»,$N`^yana .opus$` = починатися з «yana» і бути файлом «opus»$N`"try unite"` = містити точно «try unite»$N$Nформат дати - iso-8601, наприклад$N`2009-12-31` або `2020-09-12 23:30:00`", "ot_unpost": "скасувати: видалити недавні завантаження або перервати незавершені", "ot_bup": "bup: основний завантажувач, підтримує навіть netscape 4.0", "ot_mkdir": "mkdir: створити нову папку", "ot_md": "new-file: створити новий текстовий файл", //m "ot_msg": "msg: надіслати повідомлення в лог сервера", "ot_mp": "налаштування медіаплеєра", "ot_cfg": "параметри конфігурації", "ot_u2i": 'up2k: завантажити файли (якщо у вас є доступ для запису) або переключитися на режим пошуку, щоб побачити, чи існують вони десь на сервері$N$Nзавантаження можна поновлювати, багатопотокові, і часові мітки файлів зберігаються, але використовує більше CPU ніж [🎈]  (основний завантажувач)

          під час завантаження ця іконка стає індикатором прогресу!', "ot_u2w": 'up2k: завантажити файли з підтримкою поновлення (закрийте браузер і перетягніть ті самі файли пізніше)$N$Nбагатопотокові, і часові мітки файлів зберігаються, але використовує більше CPU ніж [🎈]  (основний завантажувач)

          під час завантаження ця іконка стає індикатором прогресу!', "ot_noie": 'Будь ласка, використовуйте Chrome / Firefox / Edge', "ab_mkdir": "створити папку", "ab_mkdoc": "новий текстовий файл", //m "ab_msg": "надіслати повідомлення в лог сервера", "ay_path": "перейти до папок", "ay_files": "перейти до файлів", "wt_ren": "перейменувати вибрані елементи$NГаряча клавіша: F2", "wt_del": "видалити вибрані елементи$NГаряча клавіша: ctrl-K", "wt_cut": "вирізати вибрані елементи <small>(потім вставити в іншому місці)</small>$NГаряча клавіша: ctrl-X", "wt_cpy": "копіювати вибрані елементи до буфера$N(щоб вставити їх в іншому місці)$NГаряча клавіша: ctrl-C", "wt_pst": "вставити раніше вирізане / скопійоване$NГаряча клавіша: ctrl-V", "wt_selall": "вибрати всі файли$NГаряча клавіша: ctrl-A (коли фокус на файлі)", "wt_selinv": "інвертувати вибір", "wt_zip1": "завантажити цю папку як архів", "wt_selzip": "завантажити вибір як архів", "wt_seldl": "завантажити вибір як окремі файли$NГаряча клавіша: Y", "wt_npirc": "копіювати інформацію треку у форматі irc", "wt_nptxt": "копіювати інформацію треку у текстовому форматі", "wt_m3ua": "додати до m3u плейлисту (потім клацніть 📻копіювати)", "wt_m3uc": "копіювати m3u плейлист до буфера", "wt_grid": "перемкнути сітку / список$NГаряча клавіша: G", "wt_prev": "попередній трек$NГаряча клавіша: J", "wt_play": "відтворити / пауза$NГаряча клавіша: P", "wt_next": "наступний трек$NГаряча клавіша: L", "ul_par": "паралельні завантаження:", "ut_rand": "випадкові імена файлів", "ut_u2ts": "копіювати часову мітку останньої зміни$Nз вашої файлової системи на сервер\">📅", "ut_ow": "перезаписати існуючі файли на сервері?$N🛡️: ніколи (замість цього створить нове ім'я файлу)$N🕒: перезаписати, якщо файл на сервері старіший за ваш$N♻️: завжди перезаписувати, якщо файли відрізняються$N⏭️: безумовно пропускати всі наявні файли", //m "ut_mt": "продовжувати хешування інших файлів під час завантаження$N$Nможливо, вимкніть, якщо ваш CPU або HDD є вузьким місцем", "ut_ask": 'запитати підтвердження перед початком завантаження">💭', "ut_pot": "покращити швидкість завантаження на повільних пристроях$Nроблячи інтерфейс менш складним", "ut_srch": "не завантажувати, а перевірити, чи файли вже $N існують на сервері (сканує всі папки, які ви можете читати)", "ut_par": "призупинити завантаження, встановивши 0$N$Nзбільшіть, якщо ваше з'єднання повільне / висока затримка$N$Nзалишіть 1 в локальній мережі або якщо HDD сервера є вузьким місцем", "ul_btn": "перетягніть файли / папки
          (або клацніть сюди)", "ul_btnu": "ЗАВАНТАЖИТИ", "ul_btns": "П О Ш У К", "ul_hash": "хеш", "ul_send": "надіслати", "ul_done": "готово", "ul_idle1": "завантаження ще не поставлені в чергу", "ut_etah": "середня швидкість <em>хешування</em> і орієнтовний час до завершення", "ut_etau": "середня швидкість <em>завантаження</em> і орієнтовний час до завершення", "ut_etat": "середня <em>загальна</em> швидкість і орієнтовний час до завершення", "uct_ok": "успішно завершено", "uct_ng": "невдало: помилка / відхилено / не знайдено", "uct_done": "ok і ng разом", "uct_bz": "хешування або завантаження", "uct_q": "очікує, в черзі", "utl_name": "ім'я файлу", "utl_ulist": "список", "utl_ucopy": "копіювати", "utl_links": "посилання", "utl_stat": "статус", "utl_prog": "прогрес", // keep short: "utl_404": "404", "utl_err": "ПОМИЛКА", "utl_oserr": "помилка ОС", "utl_found": "знайдено", "utl_defer": "відкласти", "utl_yolo": "YOLO", "utl_done": "готово", "ul_flagblk": "файли були додані до черги
          однак є зайнятий up2k в іншій вкладці браузера,
          тому чекаємо, поки він завершиться спочатку", "ul_btnlk": "конфігурація сервера заблокувала цей перемикач у цьому стані", "udt_up": "Завантаження", "udt_srch": "Пошук", "udt_drop": "перетягніть сюди", "u_nav_m": '
          гаразд, що у вас є?
          Enter = Файли (один або більше)\nESC = Одна папка (включаючи підпапки)', "u_nav_b": 'ФайлиОдна папка', "cl_opts": "перемикачі", "cl_hfsz": "розмір файлу", //m "cl_themes": "тема", "cl_langs": "мова", "cl_ziptype": "завантаження папки", "cl_uopts": "перемикачі up2k", "cl_favico": "favicon", "cl_bigdir": "великі папки", "cl_hsort": "#сортування", "cl_keytype": "позначення клавіш", "cl_hiddenc": "приховані стовпці", "cl_hidec": "приховати", "cl_reset": "скинути", "cl_hpick": "натисніть на заголовки стовпців, щоб приховати їх у таблиці нижче", "cl_hcancel": "приховання стовпців скасовано", "cl_rcm": "контекстне меню", //m "ct_grid": '田 сітка', "ct_ttips": '◔ ◡ ◔">ℹ️ підказки', "ct_thumb": 'у режимі сітки, перемкнути іконки або мініатюри$NГаряча клавіша: T">🖼️ мініатюри', "ct_csel": 'використовувати CTRL і SHIFT для вибору файлів у режимі сітки">вибір', "ct_dsel": 'використовувати вибір перетягуванням у режимі сітки">перетягнути', //m "ct_dl": 'примусове завантаження (не показувати вбудовано) під час натискання на файл">dl', //m "ct_ihop": 'коли переглядач зображень закрито, прокрутити вниз до останнього переглянутого файлу">g⮯', "ct_dots": 'показати приховані файли (якщо сервер дозволяє)">приховані файли', "ct_qdel": 'при видаленні файлів, запитати підтвердження лише один раз">швидке видалення', "ct_dir1st": 'сортувати папки перед файлами">спочатку 📁', "ct_nsort": 'природне сортування (для імен файлів з початковими цифрами)">природне сортування', "ct_utc": 'використовуйте UTC для всіх часових позначень">UTC', //m "ct_readme": 'показати README.md у списках папок">📜 readme', "ct_idxh": 'показати index.html замість списку папки">htm', "ct_sbars": 'показати смуги прокрутки">⟊', "cut_umod": "якщо файл вже існує на сервері, оновити часову мітку останньої зміни сервера відповідно до вашого локального файлу (потребує дозволів на запис+видалення)\">re📅", "cut_turbo": "кнопка yolo, ви, ймовірно, НЕ хочете її вмикати:$N$Nвикористовуйте це, якщо ви завантажували величезну кількість файлів і змушені були перезапустити з якоїсь причини, і хочете продовжити завантаження якнайшвидше$N$Nце замінює перевірку хешу простою "чи має цей файл той самий розмір на сервері?", тому якщо вміст файлу відрізняється, він НЕ буде завантажений$N$Nви повинні вимкнути це, коли завантаження буде завершено, а потім "завантажити" ті самі файли знову, щоб дозволити клієнту перевірити їх\">turbo", "cut_datechk": "не має ефекту, якщо кнопка turbo не ввімкнена$N$Nзменшує yolo фактор на крихту; перевіряє, чи відповідають часові мітки файлів на сервері вашим$N$Nтеоретично, повинно зловити більшість незавершених / пошкоджених завантажень, але не є заміною виконання перевірки з вимкненим turbo потім\">date-chk", "cut_u2sz": "розмір (у MiB) кожного фрагмента завантаження; великі значення краще летять через атлантичний океан. Спробуйте низькі значення на дуже ненадійних з'єднаннях", "cut_flag": "переконатися, що лише одна вкладка завантажує одночасно $N -- інші вкладки також повинні мати це ввімкнене $N -- впливає лише на вкладки на тому самому домені", "cut_az": "завантажувати файли в алфавітному порядку, а не від найменшого файлу$N$Nалфавітний порядок може полегшити огляд, якщо щось пішло не так на сервері, але робить завантаження трохи повільнішим на fiber / LAN", "cut_nag": "сповіщення ОС після завершення завантаження$N(тільки якщо браузер або вкладка не активні)", "cut_sfx": "звуковий сигнал після завершення завантаження$N(тільки якщо браузер або вкладка не активні)", "cut_mt": "використовувати багатопотоковість для прискорення хешування файлів$N$Nце використовує веб-воркери і потребує$Nбільше пам'яті (до 512 MiB додатково)$N$Nробить https на 30% швидше, http у 4.5 рази швидше\">mt", "cut_wasm": "використовувати wasm замість вбудованого хешера браузера; покращує швидкість у браузерах на базі chrome, але збільшує навантаження CPU, плюс багато старих версій chrome мають баги, які змушують браузер споживати всю пам'ять і вилітати, якщо ця опція ввімкненаа\">wasm", "cft_text": "текст favicon (порожній і оновити для відключення)", "cft_fg": "колір переднього плану", "cft_bg": "колір фону", "cdt_lim": "максимальна кількість файлів для показу в папці", "cdt_ask": "при прокрутці до низу,$Nзамість завантаження більше файлів,$Nзапитати, що робити", "cdt_hsort": "`скільки правил сортування (`,sorthref`) включати в медіа-URL. Встановлення цього в 0 також буде ігнорувати правила сортування, включені в медіа посилання при їх натисканні", "cdt_ren": "увімкнути користувацьке контекстне меню, звичайне меню доступне при натисканні shift і правої кнопки миші\">увімкнути", //m "cdt_rdb": "показувати звичайне меню правої кнопки, якщо власне меню вже відкрите і відбувається повторне клацання\">x2", //m "tt_entree": "показати панель навігації (бічна панель дерева каталогів)$NГаряча клавіша: B", "tt_detree": "показати хлібні крихти$NГаряча клавіша: B", "tt_visdir": "прокрутити до вибраної папки", "tt_ftree": "перемкнути дерево папок / текстові файли$NГаряча клавіша: V", "tt_pdock": "показати батьківські папки в закріпленій панелі зверху", "tt_dynt": "автоматично збільшуватися при розширенні дерева", "tt_wrap": "перенесення слів", "tt_hover": "показувати переповнені рядки при наведенні$N( порушує прокрутку, якщо курсор $N  миші не знаходиться в лівому відступі )", "ml_pmode": "в кінці папки...", "ml_btns": "команди", "ml_tcode": "транскодувати", "ml_tcode2": "транскодувати в", "ml_tint": "відтінок", "ml_eq": "аудіо еквалайзер", "ml_drc": "компресор динамічного діапазону", "ml_ss": "пропускати тишу", //m "mt_loop": "зациклити/повторити одну пісню\">🔁", "mt_one": "зупинити після однієї пісні\">1️⃣", "mt_shuf": "перемішати пісні в кожній папці\">🔀", "mt_aplay": "автовідтворення, якщо є ID пісні в посиланні, по якому ви клацнули для доступу до сервера$N$Nвідключення цього також зупинить оновлення URL сторінки з ID пісень під час відтворення музики, щоб запобігти автовідтворенню, якщо ці налаштування втрачені, але URL залишається\">a▶", "mt_preload": "почати завантаження наступної пісні ближче до кінця для безперервного відтворення\">preload", "mt_prescan": "перейти до наступної папки перед тим, як остання пісня$Nзакінчиться, підтримуючи веб-браузер у робочому стані$Nщоб він не зупинив відтворення\">nav", "mt_fullpre": "спробувати попередньо завантажити всю пісню;$N✅ увімкніть на ненадійних з'єднаннях,$N❌ вимкніть на повільних з'єднаннях, ймовірно\">full", "mt_fau": "на телефонах, запобігти зупинці музики, якщо наступна пісня не завантажується достатньо швидко (може зробити відображення тегів глючним)\">☕️", "mt_waves": "смуга хвильової форми:$Nпоказати амплітуду аудіо в повзунку\">~s", "mt_npclip": "показати кнопки для копіювання до буфера поточної пісні, що відтворюється\">/np", "mt_m3u_c": "показати кнопки для копіювання до буфера$Nвибраних пісень як записи плейлисту m3u8\">📻", "mt_octl": "інтеграція з ОС (медіа гарячі клавіші / osd)\">os-ctl", "mt_oseek": "дозволити перемотування через інтеграцію з ОС$N$Nзауваження: на деяких пристроях (iPhone),$Nце замінює кнопку наступної пісні\">seek", "mt_oscv": "показати обкладинку альбому в osd\">art", "mt_follow": "тримати трек, що відтворюється, у полі зору\">🎯", "mt_compact": "компактні елементи керування\">⟎", "mt_uncache": "очистити кеш  (спробуйте це, якщо ваш браузер закешував$Nпошкоджену копію пісні, тому відмовляється її відтворювати)\">uncache", "mt_mloop": "зациклити відкриту папку\">🔁 loop", "mt_mnext": "завантажити наступну папку і продовжити\">📂 next", "mt_mstop": "зупинити відтворення\">⏸ stop", "mt_cflac": "конвертувати flac / wav в {0}\">flac", "mt_caac": "конвертувати aac / m4a в {0}\">aac", "mt_coth": "конвертувати всі інші (не mp3) в {0}\">oth", "mt_c2opus": "найкращий вибір для робочих столів, ноутбуків, android\">opus", "mt_c2owa": "opus-weba, для iOS 17.5 і новіших\">owa", "mt_c2caf": "opus-caf, для iOS 11 до 17\">caf", "mt_c2mp3": "використовуйте це на дуже старих пристроях\">mp3", "mt_c2flac": "найкраща якість звуку, але великі завантаження\">flac", //m "mt_c2wav": "відтворення без стиснення (ще більше)\">wav", //m "mt_c2ok": "гарно, хороший вибір", "mt_c2nd": "це не рекомендований вихідний формат для вашого пристрою, але це нормально", "mt_c2ng": "ваш пристрій, здається, не підтримує цей вихідний формат, але давайте все одно спробуємо", "mt_xowa": "є баги в iOS, які запобігають фоновому відтворенню з використанням цього формату; будь ласка, використовуйте caf або mp3 замість цього", "mt_tint": "рівень фону (0-100) на смузі перемотування$Nщоб зробити буферизацію менш відвертаючою", "mt_eq": "`вмикає еквалайзер і контроль посилення;$N$Nпосилення `0` = стандартна 100% гучність (немодифікована)$N$Nширина `1  ` = стандартне стерео (немодифіковане)$Nширина `0.5` = 50% перехресне живлення ліво-право$Nширина `0  ` = моно$N$Nпосилення `-0.8` & ширина `10` = видалення вокалу :^)$N$Nвключення еквалайзера робить безшовні альбоми повністю безшовними, тому залишайте його увімкненим з усіма значеннями в нулі (окрім ширини = 1), якщо вам це важливо", "mt_drc": "вмикає компресор динамічного діапазону (вирівнювач гучності / цегловий вал); також увімкне EQ для балансування спагеті, тому встановіть всі поля EQ окрім 'width' в 0, якщо ви цього не хочете$N$Nзнижує гучність аудіо вище THRESHOLD дБ; для кожного RATIO дБ понад THRESHOLD є 1 дБ виходу, тому стандартні значення tresh -24 і ratio 12 означають, що він ніколи не повинен стати гучнішим за -22 дБ і безпечно збільшити посилення еквалайзера до 0.8, або навіть 1.8 з ATK 0 і величезним RLS як 90 (працює тільки в firefox; RLS максимум 1 в інших браузерах)$N$N(дивіться вікіпедію, вони пояснюють це набагато краще)", "mt_ss": "`вмикає пропуск тиші; множить швидкість на `шв` біля початку/кінця, коли гучність нижче `гуч` і позиція в перших `поч`% або останніх `кін`%", //m "mt_ssvt": "поріг гучності (0-255)\">гуч", //m "mt_ssts": "активний поріг (% треку, початок)\">поч", //m "mt_sste": "активний поріг (% треку, кінець)\">кін", //m "mt_sssm": "множник швидкості відтворення\">шв", //m "mb_play": "відтворити", "mm_hashplay": "відтворити цей аудіо файл?", "mm_m3u": "натисніть Enter/Гаразд для відтворення\nнатисніть ESC/Скасувати для редагування", "mp_breq": "потрібен firefox 82+ або chrome 73+ або iOS 15+", "mm_bload": "зараз завантажується...", "mm_bconv": "конвертується в {0}, будь ласка, зачекайте...", "mm_opusen": "ваш браузер не може відтворювати aac / m4a файли;\nтранскодування в opus тепер увімкнено", "mm_playerr": "відтворення невдале: ", "mm_eabrt": "Спроба відтворення була скасована", "mm_enet": "Ваше інтернет-з'єднання нестабільне", "mm_edec": "Цей файл нібито пошкоджений??", "mm_esupp": "Ваш браузер не розуміє цей аудіо формат", "mm_eunk": "Невідома помилка", "mm_e404": "Не вдалося відтворити аудіо; помилка 404: Файл не знайдено.", "mm_e403": "Не вдалося відтворити аудіо; помилка 403: Доступ заборонено.\n\nСпробуйте натиснути F5 для перезавантаження, можливо, ви вийшли з системи", "mm_e415": "Не вдалося відтворити аудіо; помилка 415: Не вдалося перетворити файл; перевірте логи сервера.", //m "mm_e500": "Не вдалося відтворити аудіо; помилка 500: Перевірте логи сервера.", "mm_e5xx": "Не вдалося відтворити аудіо; помилка сервера ", "mm_nof": "не знаходжу більше аудіо файлів поблизу", "mm_prescan": "Шукаю музику для наступного відтворення...", "mm_scank": "Знайшов наступну пісню:", "mm_uncache": "кеш очищено; всі пісні будуть перезавантажені при наступному відтворенні", "mm_hnf": "ця пісня більше не існує", "im_hnf": "це зображення більше не існує", "f_empty": 'ця папка порожня', "f_chide": 'це приховає стовпець «{0}»\n\nви можете показати стовпці в вкладці налаштувань', "f_bigtxt": "цей файл розміром {0} MiB -- дійсно переглядати як текст?", "f_bigtxt2": "переглянути лише кінець файлу замість цього? це також увімкне відслідковування/tailing, показуючи новододані рядки тексту в реальному часі", "fbd_more": '
          показано {0} з {1} файлів; показати {2} або показати всі
          ', "fbd_all": '
          показано {0} з {1} файлів; показати всі
          ', "f_anota": "лише {0} з {1} елементів було вибрано;\nщоб вибрати всю папку, спочатку прокрутіть до низу", "f_dls": 'посилання на файли в поточній папці були\nзмінені на посилання для завантаження', "f_dl_nd": 'пропуск папки (скористайтеся завантаженням zip/tar замість цього):\n', //m "f_partial": "Щоб безпечно завантажити файл, який зараз завантажується, будь ласка, клацніть на файл, який має таке саме ім'я, але без розширення .PARTIAL. Будь ласка, натисніть Скасувати або Escape, щоб зробити це.\n\nНатиснення Гаразд / Enter проігнорує це попередження і продовжить завантаження .PARTIAL робочого файлу замість цього, що майже напевно дасть вам пошкоджені дані.", "ft_paste": "вставити {0} елементів$NГаряча клавіша: ctrl-V", "fr_eperm": 'не можу перейменувати:\nу вас немає дозволу “переміщення“ в цій папці', "fd_eperm": 'не можу видалити:\nу вас немає дозволу “видалення“ в цій папці', "fc_eperm": 'не можу вирізати:\nу вас немає дозволу “переміщення“ в цій папці', "fp_eperm": 'не можу вставити:\nу вас немає дозволу “запису“ в цій папці', "fr_emore": "виберіть принаймні один елемент для перейменування", "fd_emore": "виберіть принаймні один елемент для видалення", "fc_emore": "виберіть принаймні один елемент для вирізання", "fcp_emore": "виберіть принаймні один елемент для копіювання до буфера", "fs_sc": "поділитися папкою, в якій ви знаходитесь", "fs_ss": "поділитися вибраними файлами", "fs_just1d": "ви не можете вибрати більше однієї папки,\nабо змішувати файли і папки в одному виборі", "fs_abrt": "❌ скасувати", "fs_rand": "🎲 випадк.ім'я", "fs_go": "✅ створити спільний доступ", "fs_name": "ім'я", "fs_src": "джерело", "fs_pwd": "пароль", "fs_exp": "термін дії", "fs_tmin": "хв", "fs_thrs": "годин", "fs_tdays": "днів", "fs_never": "вічний", "fs_pname": "необов'язкове ім'я посилання; буде випадковим, якщо порожнє", "fs_tsrc": "файл або папка для спільного доступу", "fs_ppwd": "необов'язковий пароль", "fs_w8": "створення спільного доступу...", "fs_ok": "натисніть Enter/Гаразд для копіювання до буфера\nнатисніть ESC/Скасувати для закриття", "frt_dec": "може виправити деякі випадки пошкоджених імен файлів\">url-decode", "frt_rst": "скинути змінені імена файлів назад до оригінальних\">↺ reset", "frt_abrt": "перервати і закрити це вікно\">❌ cancel", "frb_apply": "ЗАСТОСУВАТИ ПЕРЕЙМЕНУВАННЯ", "fr_adv": "пакетне / метадані / шаблонне перейменування\">розширене", "fr_case": "регулярний вираз з урахуванням регістру\">регістр", "fr_win": "безпечні для windows імена; замінити <>:"\\|?* на японські повноширинні символи\">win", "fr_slash": "замінити / на символ, який не призводить до створення нових папок\">без /", "fr_re": "`шаблон пошуку регулярного виразу для застосування до оригінальних імен файлів; групи захоплення можна посилатися в полі формату нижче як `(1)` і `(2)` і так далі", "fr_fmt": "`натхненний foobar2000:$N`(title)` замінюється назвою пісні,$N`[(artist) - ](title)` пропускає [цю] частину, якщо виконавець порожній$N`$lpad((tn),2,0)` доповнює номер треку до 2 цифр", "fr_pdel": "видалити", "fr_pnew": "зберегти як", "fr_pname": "надайте ім'я для вашого нового пресету", "fr_aborted": "перервано", "fr_lold": "старе ім'я", "fr_lnew": "нове ім'я", "fr_tags": "теги для вибраних файлів (тільки для читання, лише для довідки):", "fr_busy": "перейменування {0} елементів...\n\n{1}", "fr_efail": "перейменування невдале:\n", "fr_nchg": "{0} з нових імен були змінені через win та/або no /\n\nOK продовжити з цими зміненими новими іменами?", "fd_ok": "видалення OK", "fd_err": "видалення невдале:\n", "fd_none": "нічого не було видалено; можливо, заблоковано конфігурацією сервера (xbd)?", "fd_busy": "видалення {0} елементів...\n\n{1}", "fd_warn1": "ВИДАЛИТИ ці {0} елементи?", "fd_warn2": "Останній шанс! Неможливо скасувати. Видалити?", "fc_ok": "вирізано {0} елементів", "fc_warn": 'вирізано {0} елементів\n\nале: тільки ця вкладка браузера може їх вставити\n(оскільки вибір настільки величезний)', "fcc_ok": "скопійовано {0} елементів до буфера", "fcc_warn": 'скопійовано {0} елементів до буфера\n\nале: тільки ця вкладка браузера може їх вставити\n(оскільки вибір настільки величезний)', "fp_apply": "використовувати ці імена", "fp_skip": "пропустити конфлікти", //m "fp_ecut": "спочатку вирізати або скопіювати деякі файли / папки для вставки / переміщення\n\nзауваження: ви можете вирізати / вставляти через різні вкладки браузера", "fp_ename": "{0} елементів не можуть бути переміщені сюди, тому що імена вже зайняті. Дайте їм нові імена нижче для продовження, або залиште ім'я порожнім (\"пропустити конфлікти\"), щоб пропустити їх:", //m "fcp_ename": "{0} елементів не можуть бути скопійовані сюди, тому що імена вже зайняті. Дайте їм нові імена нижче для продовження, або залиште ім'я порожнім (\"пропустити конфлікти\"), щоб пропустити їх:", //m "fp_emore": "є ще деякі конфлікти імен файлів, які потрібно виправити", "fp_ok": "переміщення OK", "fcp_ok": "копіювання OK", "fp_busy": "переміщення {0} елементів...\n\n{1}", "fcp_busy": "копіювання {0} елементів...\n\n{1}", "fp_abrt": "переривання...", //m "fp_err": "переміщення невдале:\n", "fcp_err": "копіювання невдале:\n", "fp_confirm": "перемістити ці {0} елементи сюди?", "fcp_confirm": "скопіювати ці {0} елементи сюди?", "fp_etab": 'не вдалося прочитати буфер з іншої вкладки браузера', "fp_name": "завантаження файлу з вашого пристрою. Дайте йому ім'я:", "fp_both_m": '
          виберіть, що вставити
          Enter = Перемістити {0} файлів з «{1}»\nESC = Завантажити {2} файлів з вашого пристрою', "fcp_both_m": '
          виберіть, що вставити
          Enter = Скопіювати {0} файлів з «{1}»\nESC = Завантажити {2} файлів з вашого пристрою', "fp_both_b": 'ПереміститиЗавантажити', "fcp_both_b": 'СкопіюватиЗавантажити', "mk_noname": "введіть ім'я в текстове поле зліва перед тим, як робити це :p", "nmd_i1": "ви також можете додати потрібне розширення, наприклад .md", //m "nmd_i2": "ви можете створювати тільки файли .{0}, оскільки не маєте дозволу на видалення", //m "tv_load": "Завантаження текстового документа:\n\n{0}\n\n{1}% ({2} з {3} MiB завантажено)", "tv_xe1": "не вдалося завантажити текстовий файл:\n\nпомилка ", "tv_xe2": "404, файл не знайдено", "tv_lst": "список текстових файлів в", "tvt_close": "повернутися до перегляду папки$NГаряча клавіша: M (або Esc)\">❌ закрити", "tvt_dl": "завантажити цей файл$NГаряча клавіша: Y\">💾 завантажити", "tvt_prev": "показати попередній документ$NГаряча клавіша: i\">⬆ попер", "tvt_next": "показати наступний документ$NГаряча клавіша: K\">⬇ наст", "tvt_sel": "вибрати файл   ( для вирізання / копіювання / видалення / ... )$NГаряча клавіша: S\">вибр", "tvt_j": "прикрасити json$NГаряча клавіша: shift-J\">j", //m "tvt_edit": "відкрити файл в текстовому редакторі$NГаряча клавіша: E\">✏️ редагувати", "tvt_tail": "моніторити файл на зміни; показувати нові рядки в реальному часі\">📡 слідкувати", "tvt_wrap": "перенесення слів\">↵", "tvt_atail": "заблокувати прокрутку до низу сторінки\">⚓", "tvt_ctail": "декодувати кольори терміналу (ansi escape коди)\">🌈", "tvt_ntail": "ліміт історії прокрутки (скільки байтів тексту тримати завантаженими)", "m3u_add1": "пісня додана до m3u плейлисту", "m3u_addn": "{0} пісень додано до m3u плейлисту", "m3u_clip": "m3u плейлист тепер скопійований до буфера\n\nви повинні створити новий текстовий файл з назвою щось.m3u і вставити плейлист в цей документ; це зробить його відтворюваним", "gt_vau": "не показувати відео, лише відтворювати аудіо\">🎧", "gt_msel": "увімкнути вибір файлів; ctrl-клік по файлу для перевизначення$N$N<em>коли активний: подвійний клік по файлу / папці щоб відкрити</em>$N$NГаряча клавіша: S\">мультивибір", "gt_crop": "обрізати мініатюри по центру\">обрізка", "gt_3x": "мініатюри високої роздільності\">3x", "gt_zoom": "масштаб", "gt_chop": "обрізати", "gt_sort": "сортувати за", "gt_name": "ім'ям", "gt_sz": "розміром", "gt_ts": "датою", "gt_ext": "типом", "gt_c1": "обрізати імена файлів більше (показувати менше)", "gt_c2": "обрізати імена файлів менше (показувати більше)", "sm_w8": "пошук...", "sm_prev": "результати пошуку нижче з попереднього запиту:\n ", "sl_close": "закрити результати пошуку", "sl_hits": "показано {0} результатів", "sl_moar": "завантажити більше", "s_sz": "розмір", "s_dt": "дата", "s_rd": "шлях", "s_fn": "ім'я", "s_ta": "теги", "s_ua": "up@", "s_ad": "розш.", "s_s1": "мінімум MiB", "s_s2": "максимум MiB", "s_d1": "мін. iso8601", "s_d2": "макс. iso8601", "s_u1": "завантажено після", "s_u2": "та/або до", "s_r1": "шлях містить   (розділені пробілами)", "s_f1": "ім'я містить   (заперечення з -ні)", "s_t1": "теги містять   (^=початок, кінець=$)", "s_a1": "специфічні властивості метаданих", "md_eshow": "не можу відобразити ", "md_off": "[📜readme] відключено в [⚙️] -- документ прихований", "badreply": "Не вдалося обробити відповідь сервера", "xhr403": "403: Доступ заборонено\n\nспробуйте натиснути F5, можливо ви вийшли з системи", "xhr0": "невідома (ймовірно втрачено з'єднання з сервером, або сервер офлайн)", "cf_ok": "вибачте за це -- захист від DD" + wah + "oS спрацював\n\nречі повинні відновитися приблизно через 30 сек\n\nякщо нічого не відбувається, натисніть F5 для перезавантаження сторінки", "tl_xe1": "не вдалося перелічити підпапки:\n\nпомилка ", "tl_xe2": "404: Папка не знайдена", "fl_xe1": "не вдалося перелічити файли в папці:\n\nпомилка ", "fl_xe2": "404: Папка не знайдена", "fd_xe1": "не вдалося створити підпапку:\n\nпомилка ", "fd_xe2": "404: Батьківська папка не знайдена", "fsm_xe1": "не вдалося надіслати повідомлення:\n\nпомилка ", "fsm_xe2": "404: Батьківська папка не знайдена", "fu_xe1": "не вдалося завантажити список unpost з сервера:\n\nпомилка ", "fu_xe2": "404: Файл не знайдено??", "fz_tar": "нестиснутий gnu-tar файл (linux / mac)", "fz_pax": "нестиснутий tar в pax-форматі (повільніше)", "fz_targz": "gnu-tar зі стисненням gzip рівня 3$N$Nце зазвичай дуже повільно, тому$Nвикористовуйте нестиснутий tar замість цього", "fz_tarxz": "gnu-tar зі стисненням xz рівня 1$N$Nце зазвичай дуже повільно, тому$Nвикористовуйте нестиснутий tar замість цього", "fz_zip8": "zip з utf8 іменами файлів (можливо нестабільний на windows 7 і старіших)", "fz_zipd": "zip з традиційними cp437 іменами файлів, для дуже старого ПЗ", "fz_zipc": "cp437 з crc32, обчисленим заздалегідь,$Nдля MS-DOS PKZIP v2.04g (жовтень 1993)$N(потребує більше часу для обробки перед початком завантаження)", "un_m1": "ви можете видалити ваші недавні завантаження (або перервати незавершені) нижче", "un_upd": "оновити", "un_m4": "або поділитися файлами, видимими нижче:", "un_ulist": "показати", "un_ucopy": "копіювати", "un_flt": "необов'язковий фільтр:  URL повинен містити", "un_fclr": "очистити фільтр", "un_derr": 'unpost-видалення невдале:\n', "un_f5": 'щось зламалося, будь ласка, спробуйте оновити або натисніть F5', "un_uf5": "вибачте, але ви повинні оновити сторінку (наприклад, натиснувши F5 або CTRL-R) перед тим, як це завантаження можна буде перервати", "un_nou": 'попередження: сервер занадто зайнятий, щоб показати незавершені завантаження; клацніть посилання "оновити" трохи пізніше', "un_noc": 'попередження: unpost повністю завантажених файлів не увімкнено/дозволено в конфігурації сервера', "un_max": "показано перші 2000 файлів (використовуйте фільтр)", "un_avail": "{0} недавніх завантажень можуть бути видалені
          {1} незавершених можуть бути перервані", "un_m2": "відсортовано за часом завантаження; найновіші спочатку:", "un_no1": "ха! немає завантажень, достатньо недавніх", "un_no2": "ха! немає завантажень, що відповідають цьому фільтру, достатньо недавніх", "un_next": "видалити наступні {0} файлів нижче", "un_abrt": "перервати", "un_del": "видалити", "un_m3": "завантаження ваших недавніх завантажень...", "un_busy": "видалення {0} файлів...", "un_clip": "{0} посилань скопійовано до буфера", "u_https1": "вам слід", "u_https2": "переключитися на https", "u_https3": "для кращої продуктивності", "u_ancient": 'ваш браузер вражаюче старий -- можливо, вам слід використовувати bup замість цього', "u_nowork": "потрібен firefox 53+ або chrome 57+ або iOS 11+", "tail_2old": "потрібен firefox 105+ або chrome 71+ або iOS 14.5+", "u_nodrop": 'ваш браузер занадто старий для перетягування завантажень', "u_notdir": "це не папка!\n\nваш браузер занадто старий,\nбудь ласка, спробуйте перетягування замість цього", "u_uri": "щоб перетягнути зображення з інших вікон браузера,\nбудь ласка, перетягніть його на велику кнопку завантаження", "u_enpot": 'переключитися на картоплинний UI (може покращити швидкість завантаження)', "u_depot": 'переключитися на вишуканий UI (може зменшити швидкість завантаження)', "u_gotpot": 'переключення на картоплинний UI для покращення швидкості завантаження,\n\nне соромтеся не погодитися і переключитися назад!', "u_pott": "

          файли:   {0} завершено,   {1} невдало,   {2} зайнято,   {3} в черзі

          ", "u_ever": "це базовий завантажувач; up2k потребує принаймні
          chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1", "u_su2k": 'це базовий завантажувач; up2k кращий', "u_uput": 'оптимізувати для швидкості (пропустити контрольну суму)', "u_ewrite": 'у вас немає доступу для запису в цю папку', "u_eread": 'у вас немає доступу для читання цієї папки', "u_enoi": 'пошук файлів не увімкнено в конфігурації сервера', "u_enoow": "перезапис не працюватиме тут; потрібен дозвіл на видалення", "u_badf": 'Ці {0} файли (з {1} загальних) були пропущені, можливо, через дозволи файлової системи:\n\n', "u_blankf": 'Ці {0} файли (з {1} загальних) порожні; все одно завантажити їх?\n\n', "u_applef": 'Ці {0} файли (з {1} загальних), ймовірно, небажані;\nНатисніть Гаразд/Enter щоб ПРОПУСТИТИ наступні файли,\nНатисніть Скасувати/ESC щоб НЕ виключати, і ЗАВАНТАЖИТИ їх також:\n\n', "u_just1": '\nМожливо, це спрацює краще, якщо ви виберете лише один файл', "u_ff_many": "якщо ви використовуєте Linux / MacOS / Android, то така кількість файлів може завісити Firefox!\nякщо це станеться, будь ласка, спробуйте знову (або використовуйте Chrome).", "u_up_life": "Це завантаження буде видалено з сервера\n{0} після його завершення", "u_asku": 'завантажити ці {0} файлів до {1}', "u_unpt": "ви можете скасувати / видалити це завантаження, використовуючи 🧯 зверху зліва", "u_bigtab": 'збираюся показати {0} файлів\n\nце може завісити ваш браузер, ви впевнені?', "u_scan": 'Сканування файлів...', "u_dirstuck": 'ітератор каталогу застряг, намагаючись отримати доступ до наступних {0} елементів; пропущу:', "u_etadone": 'Готово ({0}, {1} файлів)', "u_etaprep": '(підготовка до завантаження)', "u_hashdone": 'хешування завершено', "u_hashing": 'хешування', "u_hs": 'рукостискання...', "u_started": "файли тепер завантажуються; дивіться [🚀]", "u_dupdefer": "дублікат; буде оброблено після всіх інших файлів", "u_actx": "клацніть цей текст, щоб запобігти втраті
          продуктивності при переключенні на інші вікна/вкладки", "u_fixed": "OK!  Виправлено 👍", "u_cuerr": "не вдалося завантажити фрагмент {0} з {1};\nймовірно, нешкідливо, продовжую\n\nфайл: {2}", "u_cuerr2": "сервер відхилив завантаження (фрагмент {0} з {1});\nспробую пізніше\n\nфайл: {2}\n\nпомилка ", "u_ehstmp": "спробую знову; дивіться внизу справа", "u_ehsfin": "сервер відхилив запит на завершення завантаження; повторюю...", "u_ehssrch": "сервер відхилив запит на виконання пошуку; повторюю...", "u_ehsinit": "сервер відхилив запит на ініціацію завантаження; повторюю...", "u_eneths": "мережева помилка під час виконання рукостискання завантаження; повторюю...", "u_enethd": "мережева помилка під час тестування існування цілі; повторюю...", "u_cbusy": "чекаємо, поки сервер знову нам довірятиме після мережевого збою...", "u_ehsdf": "на сервері закінчилося місце на диску!\n\nбуду продовжувати спроби, на випадок, якщо хтось\nзвільнить достатньо місця для продовження", "u_emtleak1": "схоже, ваш веб-браузер може мати витік пам'яті;\nбудь ласка,", "u_emtleak2": ' переключіться на https (рекомендується) або ', "u_emtleak3": ' ', "u_emtleakc": 'спробуйте наступне:\n
          • натисніть F5 для оновлення сторінки
          • потім відключіть кнопку  mt  в  ⚙️ налаштуваннях
          • і спробуйте це завантаження знову
          Завантаження будуть трохи повільнішими, але що поробиш.\nВибачте за незручності !\n\nPS: chrome v107 має виправлення для цього', "u_emtleakf": 'спробуйте наступне:\n
          • натисніть F5 для оновлення сторінки
          • потім увімкніть 🥔 (картопля) в UI завантаження
          • і спробуйте це завантаження знову
          \nPS: firefox сподіваємося, матиме виправлення в якийсь момент', "u_s404": "не знайдено на сервері", "u_expl": "пояснити", "u_maxconn": "більшість браузерів обмежують це до 6, але firefox дозволяє підвищити це з connections-per-server в about:config", "u_tu": '

          ПОПЕРЕДЖЕННЯ: turbo увімкнено,  клієнт може не виявити і поновити неповні завантаження; дивіться підказку turbo-кнопки

          ', "u_ts": '

          ПОПЕРЕДЖЕННЯ: turbo увімкнено,  результати пошуку можуть бути неправильними; дивіться підказку turbo-кнопки

          ', "u_turbo_c": "turbo відключено в конфігурації сервера", "u_turbo_g": "відключаю turbo, тому що у вас немає\nпривілеїв перегляду каталогів в цьому томі", "u_life_cfg": 'автовидалення через хв (або годин)', "u_life_est": 'завантаження буде видалено ---', "u_life_max": 'ця папка забезпечує\nмакс. термін життя {0}', "u_unp_ok": 'unpost дозволено для {0}', "u_unp_ng": 'unpost НЕ буде дозволено', "ue_ro": 'ваш доступ до цієї папки тільки для читання\n\n', "ue_nl": 'ви зараз не увійшли в систему', "ue_la": 'ви зараз увійшли як "{0}"', "ue_sr": 'ви зараз в режимі пошуку файлів\n\nпереключіться на режим завантаження, клацнувши лупу 🔎 (поруч з великою кнопкою ПОШУК), і спробуйте завантажити знову\n\nвибачте', "ue_ta": 'спробуйте завантажити знову, це повинно спрацювати зараз', "ue_ab": "цей файл вже завантажується в іншу папку, і це завантаження повинно бути завершено перед тим, як файл можна буде завантажити в інше місце.\n\nВи можете перервати і забути початкове завантаження, використовуючи 🧯 зверху зліва", "ur_1uo": "OK: Файл успішно завантажено", "ur_auo": "OK: Всі {0} файлів успішно завантажено", "ur_1so": "OK: Файл знайдено на сервері", "ur_aso": "OK: Всі {0} файлів знайдено на сервері", "ur_1un": "Завантаження невдале, вибачте", "ur_aun": "Всі {0} завантажень невдалі, вибачте", "ur_1sn": "Файл НЕ знайдено на сервері", "ur_asn": "{0} файлів НЕ знайдено на сервері", "ur_um": "Завершено;\n{0} завантажень OK,\n{1} завантажень невдалих, вибачте", "ur_sm": "Завершено;\n{0} файлів знайдено на сервері,\n{1} файлів НЕ знайдено на сервері", "rc_opn": "відкрити", //m "rc_ply": "відтворити", //m "rc_pla": "відтворити як аудіо", //m "rc_txt": "відкрити у переглядачі файлів", //m "rc_md": "відкрити в текстовому редакторі", //m "rc_dl": "завантажити", //m "rc_zip": "завантажити як архів", //m "rc_cpl": "копіювати посилання", //m "rc_del": "видалити", //m "rc_cut": "вирізати", //m "rc_cpy": "копіювати", //m "rc_pst": "вставити", //m "rc_rnm": "перейменувати", //m "rc_nfo": "нова папка", //m "rc_nfi": "новий файл", //m "rc_sal": "вибрати все", //m "rc_sin": "інвертувати вибір", //m "rc_shf": "поділитися цією папкою", //m "rc_shs": "поділитися вибраним", //m "lang_set": "оновити сторінку, щоб зміни набули чинності?", "splash": { "a1": "оновити", "b1": "привітик, незнайомцю   (ви не авторизовані)", "c1": "вийти", "d1": "трасування стека", "d2": "показує стан усіх активних потоків", "e1": "перезавантажити конфіг", "e2": "перезавантажити файли конфігурації (облікові записи/томи/прапорці),$Nта пересканувати всі томи e2ds$N$Nувага: будь-які зміни глобальних налаштувань$Nвимагають повного перезапуску", "f1": "ви можете бачити:", "g1": "ви можете завантажувати файли в:", "cc1": "всяка всячина:", "h1": "вимкнути k304", "i1": "увімкнути k304", "j1": "увімкнення k304 буде відключати ваш клієнт при кожному HTTP 304, що може запобігти зависанню деяких глючних проксі (раптово перестають завантажувати сторінки), але це також зробить усе повільнішим загалом", "k1": "скинути налаштування клієнта", "l1": "авторизуйтесь для інших опцій:", "ls3": "увійти", //m "lu4": "ім'я користувача", //m "lp4": "пароль", //m "lo3": "вийти з облікового запису “{0}” всюди", //m "lo2": "це завершить сеанс у всіх браузерах", //m "m1": "з поверненням,", "n1": "404 не знайдено  ┐( ´ -`)┌", "o1": 'або у вас немає доступу -- спробуйте авторизуватися або повернутися на головну', "p1": "403 доступ заборонений  ~┻━┻", "q1": 'авторизуйтесь або поверніться на головну', "r1": "повернутися на головну", ".s1": "пересканувати", "t1": "дія", "u2": "час з останнього запису сервера$N( завантаження / перейменування / ... )$N$N17d = 17 днів$N1h23 = 1 година 23 хвилини$N4m56 = 4 хвилини 56 секунд", "v1": "підключити", "v2": "використовувати цей сервер як локальний HDD", "w1": "перейти на https", "x1": "змінити пароль", "y1": "керування доступом", "z1": "розблокувати:", "ta1": "спочатку заповніть ваш новий пароль", "ta2": "повторіть для підтвердження нового пароля:", "ta3": "описка; спробуйте знову", "nop": "ПОМИЛКА: Пароль не може бути порожнім", //m "nou": "ПОМИЛКА: Ім’я користувача та/або пароль не можуть бути порожніми", //m "aa1": "вхідні файли:", "ab1": "вимкнути no304", "ac1": "увімкнути no304", "ad1": "увімкнення no304 вимкне все кешування; спробуйте це, якщо k304 було недостатньо. Це витратить величезну кількість мережевого трафіку!", "ae1": "активні завантаження:", "af1": "показати нещодавні завантаження", "ag1": "показати відомих IdP-користувачів", } }; ================================================ FILE: copyparty/web/tl/vie.js ================================================ Ls.vie = { "tt": "Tiếng Việt", "cols": { "c": "nút hành động", "dur": "thời lượng", "q": "chất lượng / bitrate", "Ac": "codec âm thanh", "Vc": "codec video", "Fmt": "định dạng / container", "Ahash": "checksum âm thanh", "Vhash": "checksum video", "Res": "độ phân giải", "T": "loại tệp", "aq": "chất lượng âm thanh / bitrate", "vq": "chất lượng video / bitrate", "pixfmt": "subsampling / pixel structure", "resw": "độ phân giải ngang", "resh": "độ phân giải dọc", "chs": "kênh âm thanh", "hz": "tốc độ lấy mẫu", }, "hks": [ [ "misc", ["ESC", "đóng nhiều mục"], "file-manager", ["G", "chuyển đổi chế độ xem danh sách / lưới"], ["T", "chuyển đổi ảnh thu nhỏ / biểu tượng"], ["⇧ A/D", "kích thước ảnh thu nhỏ"], ["ctrl-K", "xoá mục đã chọn"], ["ctrl-X", "cắt mục đã chọn vào bảng nhớ tạm"], ["ctrl-C", "sao chép mục đã chọn vào bảng nhớ tạm"], ["ctrl-V", "dán (di chuyển/sao chép) tại đây"], ["Y", "tải xuống mục đã chọn"], ["F2", "đổi tên mục đã chọn"], "file-list-sel", ["space", "chuyển đổi chọn tệp"], ["↑/↓", "di chuyển con trỏ chọn"], ["ctrl ↑/↓", "di chuyển con trỏ và khung nhìn"], ["⇧ ↑/↓", "chọn tệp trước / sau"], ["ctrl-A", "chọn tất cả tệp / thư mục"], ], [ "navigation", ["B", "chuyển đổi đường dẫn / thanh điều hướng"], ["I/K", "thư mục trước / sau"], ["M", "thư mục cha (hoặc thu gọn hiện tại)"], ["V", "chuyển đổi thư mục / tệp văn bản trong thanh điều hướng"], ["A/D", "kích thước thanh điều hướng"], ], [ "audio-player", ["J/L", "bài trước / sau"], ["U/O", "lùi / tiến 10 giây"], ["0..9", "nhảy đến 0%..90%"], ["P", "phát/tạm dừng (cũng khởi động)"], ["S", "chọn bài đang phát"], ["Y", "tải xuống bài hát"], ], [ "image-viewer", ["J/L, ←/→", "ảnh trước / sau"], ["Home/End", "ảnh đầu / cuối"], ["F", "toàn màn hình"], ["R", "xoay theo chiều kim đồng hồ"], ["⇧ R", "xoay ngược chiều kim đồng hồ"], ["S", "chọn ảnh"], ["Y", "tải xuống ảnh"], ], [ "video-player", ["U/O", "lùi / tiến 10 giây"], ["P/K/Space", "phát/tạm dừng"], ["C", "tiếp tục phát bài tiếp theo"], ["V", "vòng lặp"], ["M", "tắt tiếng"], ["[ and ]", "đặt khoảng lặp"], ], [ "textfile-viewer", ["I/K", "tệp trước / sau"], ["M", "đóng tệp văn bản"], ["E", "chỉnh sửa tệp văn bản"], ["S", "chọn tệp (để cắt/sao chép/đổi tên)"], ] ], "m_ok": "OK", "m_ng": "Hủy", "enable": "Bật", "danger": "NGUY HIỂM", "clipped": "đã sao chép vào bảng nhớ tạm", "ht_s1": "giây", "ht_s2": "giây", "ht_m1": "phút", "ht_m2": "phút", "ht_h1": "giờ", "ht_h2": "giờ", "ht_d1": "ngày", "ht_d2": "ngày", "ht_and": " và ", "goh": "bảng điều khiển", "gop": 'thư mục trước">trước', "gou": 'thư mục cha">lên', "gon": 'thư mục sau">tiếp', "logout": "Đăng xuất ", "login": "Đăng nhập", "access": "quyền truy cập", "ot_close": "đóng menu con", "ot_search": "`tìm kiếm các tệp theo thuộc tính, đường dẫn / tên, tag nhạc hoặc bất kỳ sự kết hợp nào của chúng$N$N`foo bar` = phải chứa cả «foo» và «bar»,$N`foo -bar` = phải chứa «foo» nhưng không chứa «bar»,$N`^yana .opus$` = bắt đầu bằng «yana» và là tệp «opus»$N`"try unite"` = chứa chính xác «try unite»$N$Nđịnh dạng ngày là iso-8601, như$N`2009-12-31` hoặc `2020-09-12 23:30:00`", "ot_unpost": "unpost: xoá các tệp đã tải lên gần đây hoặc huỷ những tệp đang tải dở", "ot_bup": "bup: trình tải lên cơ bản, hỗ trợ cả Netscape 4.0", "ot_mkdir": "mkdir: tạo thư mục mới", "ot_md": "new-file: tạo tệp văn bản mới", "ot_msg": "msg: gửi tin nhắn đến nhật ký máy chủ", "ot_mp": "tuỳ chọn trình phát phương tiện", "ot_cfg": "tuỳ chọn cấu hình", "ot_u2i": 'up2k: tải tệp lên (nếu bạn có quyền ghi) hoặc chuyển sang chế độ tìm kiếm để xem chúng có tồn tại ở đâu đó trên máy chủ không$N$Ntải lên có thế tiếp tục nếu bị gián đoạn, chạy đa luồng và giữ nguyên dấu thời gian tệp, nhưng tiêu tốn nhiều CPU hơn [🎈]  (trình tải lên cơ bản)

          trong quá trình tải, biểu tượng này sẽ trở thành chỉ thị tiến trình!', "ot_u2w": 'up2k: tải tệp lên với hỗ trợ tiếp tục (đóng trình duyệt và thả lại tệp đó lên sau)$N$Nchạy đa luồng và giữ nguyên dấu thời gian tệp, nhưng tiêu tốn nhiều CPU hơn [🎈]  (trình tải lên cơ bản)

          trong quá trình tải, biểu tượng này sẽ trở thành chỉ thị tiến trình!', "ot_noie": 'Vui lòng sử dụng Chrome / Firefox / Edge', "ab_mkdir": "tạo thư mục", "ab_mkdoc": "tạo tệp văn bản", "ab_msg": "gửi tin nhắn đến nhật ký máy chủ", "ay_path": "bỏ qua đến thư mục", "ay_files": "bỏ qua đến tệp", "wt_ren": "đổi tên các mục đã chọn$NPhím tắt: F2", "wt_del": "xóa các mục đã chọn$NPhím tắt: ctrl-K", "wt_cut": "cắt các mục đã chọn <small>(sau đó dán ở nơi khác)</small>$NPhím tắt: ctrl-X", "wt_cpy": "sao chép các mục đã chọn vào bảng nhớ tạm$N(để dán ở nơi khác)$NPhím tắt: ctrl-C", "wt_pst": "dán một lựa chọn đã cắt / sao chép trước đó$NPhím tắt: ctrl-V", "wt_selall": "chọn tất cả các tệp$NPhím tắt: ctrl-A (khi tệp được chọn)", "wt_selinv": "đảo ngược lựa chọn", "wt_zip1": "tải thư mục này dưới định dạng nén", "wt_selzip": "tải lựa chọn dưới định dạng nén", "wt_seldl": "tải lựa chọn dưới dạng các tệp riêng biệt$NPhím tắt: Y", "wt_npirc": "sao chép thông tin bản nhạc theo định dạng irc", "wt_nptxt": "sao chép thông tin bản nhạc dưới dạng văn bản thuần túy", "wt_m3ua": "thêm vào danh sách phát m3u (bấm 📻copy sau)", "wt_m3uc": "sao chép danh sách phát m3u vào bảng nhớ tạm", "wt_grid": "chuyển đổi chế độ xem danh sách / lưới $NPhím tắt: G", "wt_prev": "bài trước$NPhím tắt: J", "wt_play": "phát / tạm dừng$NPhím tắt: P", "wt_next": "bài sau$NPhím tắt: L", "ul_par": "tải lên song song:", "ut_rand": "ngẫu nhiên hoá tên tệp", "ut_u2ts": "sao chép dấu thời gian chỉnh sửa cuối$Ntừ hệ thống tệp của bạn lên máy chủ\">📅", "ut_ow": "ghi đè các tệp đã có trên máy chủ?$N🛡️: không bao giờ (sẽ tạo tên tệp mới)$N🕒: ghi đè nếu tệp trên máy chủ cũ hơn$N♻️: luôn ghi đè nếu hai tệp khác nhau$N⏭️: bỏ qua vô điều kiện tất cả các tệp hiện có", //m "ut_mt": "tiếp tục hash các tệp khác trong khi tải lên$N$NCó thể tắt nếu CPU hoặc HDD của bạn bị nghẽn", "ut_ask": 'yêu cầu xác nhận trước khi bắt đầu tải lên">💭', "ut_pot": "cải thiện tốc độ tải lên trên các thiết bị chậm$Nbằng cách đơn giản hoá giao diện người dùng", "ut_srch": "không tải lên, chỉ kiểm tra xem tệp$Nđã tồn tại trên máy chủ hay chưa (sẽ quét toàn bộ thư mục bạn có quyền đọc)", "ut_par": "tạm dừng tải lên bằng cách đặt thành 0$N$NTăng lên nếu kết nối chậm hoặc độ trễ cao$N$NGiữ ở mức 1 khi dùng LAN hoặc nếu ổ cứng máy chủ bị nghẽn", "ul_btn": "thả tệp / thư mục
          ở đây (hoặc nhấn vào tôi)", "ul_btnu": "T Ả I L Ê N", "ul_btns": "T Ì M K I Ế M", "ul_hash": "hash", "ul_send": "gửi", "ul_done": "hoàn tất", "ul_idle1": "chưa có mục nào trong hàng chờ tải lên", "ut_etah": "tốc độ <em>hash</em> trung bình và thời gian dự kiến để hoàn tất", "ut_etau": "tốc độ <em>tải lên</em> trung bình và thời gian dự kiến để hoàn tất", "ut_etat": "tốc độ <em>tổng</em> trung bình và thời gian dự kiến để hoàn tất", "uct_ok": "hoàn tất thành công", "uct_ng": "không hợp lệ: lỗi / bị từ chối / không tìm thấy", "uct_done": "đã xử lý: gồm cả thành công và không hợp lệ", "uct_bz": "đang hash hoặc tải lên", "uct_q": "nhàn rỗi, đang chờ", "utl_name": "tên tệp", "utl_ulist": "danh sách", "utl_ucopy": "sao chép", "utl_links": "đường dẫn", "utl_stat": "trạng thái", "utl_prog": "tiến trình", // keep short: // phần up2k "utl_404": "404", "utl_err": "LỖI", "utl_oserr": "Lỗi hệ thống", "utl_found": "tìm thấy", "utl_defer": "hoãn", "utl_yolo": "YOLO", "utl_done": "hoàn tất", "ul_flagblk": "tệp đã được thêm vào hàng chờ
          tuy vậy đang có một tiến trình up2k đang chạy ở một tab khác
          vui lòng đợi cho đến khi tiến trình đó hoàn tất hoặc bị hủy", "ul_btnlk": "cài đặt của máy chủ đã khóa tùy chọn ở trạng thái này", "udt_up": "Tải lên", "udt_srch": "Tìm kiếm", "udt_drop": "thả vào đây", "u_nav_m": '
          chọn phương thức tải lên
          Enter = Tệp (một hoặc nhiều)\nESC = Một thư mục (kèm thư mục con)', "u_nav_b": 'TệpMột thư mục', // settings / config: "cl_opts": "tuỳ chọn", "cl_hfsz": "kích thước tệp", "cl_themes": "giao diện", "cl_langs": "ngôn ngữ", "cl_ziptype": "định dạng nén", "cl_uopts": "tuỳ chọn up2k", "cl_favico": "favicon", "cl_bigdir": "thư mục lớn", "cl_hsort": "#sắp xếp", "cl_keytype": "ghi chú bàn phím", "cl_hiddenc": "cột đã ẩn", "cl_hidec": "ẩn", "cl_reset": "đặt lại", "cl_hpick": "chạm vào tiêu đề cột để ẩn trong bảng bên dưới", "cl_hcancel": "đã hủy việc ẩn cột", "cl_rcm": "menu chuột phải", //m // settings / tuỳ chọn "ct_grid": '田 chế độ lưới', "ct_ttips": '༼ ◕_◕ ༽">ℹ️ tooltips', "ct_thumb": 'ở chế độ lưới, chuyển biểu tượng hoặc hình thu nhỏ$NPhím tắt: T">🖼️ ảnh thu nhỏ', "ct_csel": 'dùng CTRL và SHIFT để chọn tệp trong chế độ lưới">sel', "ct_dsel": 'dùng chọn bằng cách kéo trong chế độ lưới">kéo', //m "ct_dl": 'cưỡng chế tải xuống (không hiện thị trong dòng) khi nhấp vào tệp">dl', "ct_ihop": 'khi đóng trình xem ảnh, cuộn xuống tệp đã xem gần nhất">g⮯', "ct_dots": 'hiển thị tệp ẩn (nếu máy chủ cho phép)">dotfiles', "ct_qdel": 'khi xóa tệp, chỉ hỏi xác nhận một lần">qdel', "ct_dir1st": 'sắp xếp thư mục trước tệp">📁 first', "ct_nsort": 'sắp xếp tự nhiên (cho tên tệp có số ở đầu)">nsort', "ct_utc": 'hiển thị mọi thời gian theo UTC">UTC', "ct_readme": 'hiển thị README.md trong danh sách thư mục">📜 readme', "ct_idxh": 'hiển thị index.html thay cho danh sách thư mục">htm', "ct_sbars": 'hiển thị thanh cuộn">⟊', // tuỳ chọn up2k "cut_umod": "nếu tệp đã tồn tại trên máy chủ, cập nhật dấu thời gian chỉnh sửa cuối của máy chủ cho khớp với tệp cục bộ của bạn (yêu cầu quyền ghi và xóa)\">re📅", "cut_turbo": "nút YOLO, bạn gần như KHÔNG nên bật tuỳ chọn này:$N$Ndùng khi bạn đang tải lên một lượng tệp rất lớn và phải khởi động lại vì lý do nào đó, và muốn tiếp tục tải càng sớm sàng tốt$N$Ntuỳ chọn này thay thế kiểm tra hash bằng kiểm tra "kích thước tệp ở trên máy chủ có giống nhau không" nên nếu nội dung tệp khác nhau thì sẽ KHÔNG được tải lên$N$Nbạn nên tắt tuỳ chọn này sau khi tải lên xong, và "tải lên" lại các tệp đó để xác minh\">turbo", "cut_datechk": "không có tác dụng trừ khi nút turbo được bật$N$Ngiảm mức độ yolo một chút; kiểm tra xem dấu thời gian tệp trên máy chủ có khớp với của bạn không$N$Nnên về lý thuyết có thể phát hiện phần lớn tệp chưa xong hoặc bị lỗi, nhưng không thể thay thế cho việc chạy xác minh sau khi tắt turbo\">date-chk", "cut_u2sz": "kích thước (tính theo MiB) của mỗi khối tải lên; dùng kích thước khối lớn khi truyền ở khoảng cách lục địa. Hãy thử giá trị nhỏ hơn với kết nối không ổn định", "cut_flag": "đảm bảo chỉ một tab được tải lên tại một thời điểm $N -- tab khác cũng cần bật tùy chọn này $N -- tác dụng trong các tab cùng tên miền", "cut_az": "tải tệp theo thứ tự bảng chữ cái thay vì tệp nhỏ trước$N$Ntải lên theo thứ tự bảng chữ cái giúp dễ quan sát nếu có vấn đề trên máy chủ, nhưng làm tốc độ tải chậm hơn trên mạng cáp quang hoặc LAN", "cut_nag": "thông báo của hệ điều hành khi tải lên hoàn tất$N(chỉ khi trình duyệt hoặc tab không hoạt động)", "cut_sfx": "âm báo khi tải lên hoàn tất$N(chỉ khi trình duyệt hoặc tab không hoạt động)", "cut_mt": "dùng đa luồng để tăng tốc hash tệp$N$dùng web workers và cần $nhiều RAM hơn (tối đa thêm 512 MiB)$N$làm cho https nhanh hơn 30%, http nhanh hơn 4.5 lần\">mt", "cut_wasm": "dùng wasm thay vì bộ hash tích hợp của trình duyệt; nhanh hơn trên trình duyệt chromium nhưng làm tăng tải CPU, và nhiều bản chrome cũ có lỗi khiến trình duyệt dùng hết RAM và treo nếu bật tuỳ chọn này\">wasm", // favicon "cft_text": "chuỗi favicon (để trống và làm mới trang để tắt)", "cft_fg": "màu chữ", "cft_bg": "màu nền", // big dirs "cdt_lim": "số tệp tối đa hiển thị trong thư mục", "cdt_ask": "khi cuộn xuống cuối,$Nthay vì tải thêm tệp,$Nhỏi người dùng muốn làm gì", "cdt_hsort": "`số lượng luật sắp xếp(`,sorthref`) được đưa vào URL media. Đặt bằng 0 cũng sẽ bỏ qua các quy tắc sắp xếp trong liên kết media khi nhấp vào chúng", "cdt_ren": "bật menu chuột phải tùy chỉnh, menu mặc định vẫn truy cập được bằng shift + chuột phải\">bật", //m "cdt_rdb": "hiển thị menu chuột phải thông thường khi menu tùy chỉnh đã mở và nhấp chuột phải lần nữa\">x2", //m "tt_entree": "hiển thị thanh điều hướng (cây thư mục)$NPhím tắt: B", "tt_detree": "hiển thị đường dẫn$NPhím tắt: B", "tt_visdir": "cuộn đến thư mục đã chọn", "tt_ftree": "chuyển đổi cây thư mục / tệp văn bản$NPhím tắt: V", "tt_pdock": "hiển thị thư mục cha trong thanh ghim trên cùng", "tt_dynt": "tự mở rộng khi cây mở rộng", "tt_wrap": "ngắt dòng", "tt_hover": "hiện thị dòng tràn khi rê chuột$N( không cuộn được nếu $N  con trỏ chuột nằm ngoài cột trái )", "ml_pmode": "ở cuối thư mục...", "ml_btns": "lệnh", "ml_tcode": "mã hoá lại", "ml_tcode2": "mã hoá lại thành", "ml_tint": "tô màu", "ml_eq": "bộ cân bằng âm thanh", "ml_drc": "bộ nén dải động", "ml_ss": "bỏ qua khoảng lặng", //m "mt_loop": "lặp lại một bài\">🔁", "mt_one": "dừng sau một bài\">1️⃣", "mt_shuf": "trộn các bài trong thư mụcr\">🔀", "mt_aplay": "tự động phát nếu có ID bài trong link bạn nhấp để truy cập máy chủ$N$Ntắt tuỳ chọn sẽ ngăn URL của trang cập nhật theo ID bài khi phát nhạc, tránh tự động phát nếu cài đặt mất nhưng URL còn\">a▶", "mt_preload": "bắt đầu tải bài hát tiếp theo khi gần hết bài để phát liền mạch\">preload", "mt_prescan": "chuyển đến thư mục tiếp theo trước khi bài cuối cùng $Nkết thúc, giúp giữ trình duyệt hoạt động $N và không dừng phát nhạc\">nav", "mt_fullpre": "cố gắng tải trước toàn bộ bài;$N✅ bật với kết nối không ổn định,$N❌ tắt với kết nối chậm\">full", "mt_fau": "trên điện thoại, ngăn nhạc dừng nếu bài tiếp theo tải chậm (có thể gây lỗi hiển thị tag nhạc)\">☕️", "mt_waves": "thanh tiến trình bài hát dạng sóng:$Nhiển thị biên độ âm thanh trong thanh tiến trình\">~s", "mt_npclip": "hiển thị nút để sao chép bài đang phát\">/np", "mt_m3u_c": "hiển thị nút để sao chép $Nnhững bài đã chọn dưới dạng danh sách phát m3u8\">📻", "mt_octl": "tích hợp hệ điều hành (phím tắt media / OSD)\">os-ctl", "mt_oseek": "cho phép tìm kiếm qua tích hợp hệ điều hành$N$Nlưu ý: trên một số thiết bị (iphone), $Nthao tác này sẽ thay thế nút bài hát tiếp theo\">seek", "mt_oscv": "hiển thị bài album trên OSD\">art", "mt_follow": "giữ bài đang phát trong tầm nhìn\">🎯", "mt_compact": "giao diện điều khiển thu gọn\">⟎", "mt_uncache": "xoá bộ nhớ đệm  (thử nếu trình duyệt lưu trữ đệm $Nmột bản nhạc bị lỗi và không thể phát)\">uncache", "mt_mloop": "lặp trong thư mục đang mở\">🔁 loop", "mt_mnext": "tải thư mục tiếp theo và tiếp tục\">📂 next", "mt_mstop": "dừng phát\">⏸ stop", "mt_cflac": "chuyển flac / wav sang {0}\">flac", "mt_caac": "chuyển aac / m4a sang {0}\">aac", "mt_coth": "chuyển mọi loại khác (trừ mp3) thành {0}\">oth", "mt_c2opus": "lựa chọn tốt nhất cho máy tính, laptop, android\">opus", "mt_c2owa": "opus-weba, cho iOS 17.5 trở lên\">owa", "mt_c2caf": "opus-caf, cho iOS 11 đến 17\">caf", "mt_c2mp3": "dùng trên thiết bị cũ\">mp3", "mt_c2flac": "chất lượng âm thanh tốt nhất, nhưng tệp tải xuống lớn\">flac", "mt_c2wav": "phát không nén (tệp còn lớn hơn nữa)\">wav", "mt_c2ok": "lựa chọn hợp lý", "mt_c2nd": "không phải định dạng khuyến nghị cho thiết bị, nhưng hãy thử nếu bạn muốn", "mt_c2ng": "thiết bị dường như không hỗ trợ định dạng này, nhưng hãy thử nếu bạn muốn", "mt_xowa": "có một vài lỗi trên iOS ngăn phát nền với định dạng này; vui lòng dùng caf hoặc mp3", "mt_tint": "mức nền (0-100) trên thanh tiến trình", "mt_eq": "`bật bộ cân bằng âm thanh và bộ tăng ích;$N$Nboost `0  ` = âm lượng chuẩn 100% (không chỉnh)$N$Nwidth `1  ` = stereo chuẩn (không chỉnh)$Nwidth `0.5` = 50% pha trái-phải$Nwidth `0  ` = mono$N$Nboost `-0.8` & width `10` = loại bỏ lời hát :^)$N$Nbật EQ giúp cho album được phát liền mạch không ngắt quãng, nên giữ các giá trị bằng 0 (trừ width = 1) nếu bạn không muốn thay đổi âm thanh gốc", "mt_drc": "bật bộ nén dải động (làm phẳng âm lượng / brickwaller); cũng bật EQ để cân bằng, nên đặt tất cả EQ trừ 'width' = 0 nếu không muốn$N$Ngiảm âm thanh trên THRESHOLD dB; với mỗi RATIO dB vượt THRESHOLD thì có 1 dB đầu ra, ví dụ tresh -24 và ratio 12 => âm lượng không vượt -22 dB, có thể tăng EQ boost lên 0.8 hoặc 1.8 với ATK 0 và RLS lớn 90 (chỉ Firefox; RLS max 1 trên browser khác)$N$NXem Wikipedia để hiểu chi tiết hơn", "mt_ss": "`bật bỏ qua im lặng; nhân tốc độ phát với `nh` gần đầu/cuối khi âm lượng dưới `am` và vị trí trong `dau`% đầu hoặc `cuoi`% cuối", //m "mt_ssvt": "ngưỡng âm lượng (0-255)\">am", //m "mt_ssts": "ngưỡng hoạt động (% bài, đầu)\">dau", //m "mt_sste": "ngưỡng hoạt động (% bài, cuối)\">cuoi", //m "mt_sssm": "hệ số tốc độ phát\">nh", //m "mb_play": "phát", "mm_hashplay": "phát bản nhạc này?", "mm_m3u": "bấm Enter/OK để phát\nbấm ESC/Cancel để chỉnh sửa", "mp_breq": "cần firefox 82+ hoặc chrome 73+ hoặc iOS 15+", "mm_bload": "đang tải...", "mm_bconv": "đang chuyển đổi sang {0}, vui lòng chờ...", "mm_opusen": "trình duyệt không hỗ trợ tệp aac / m4a;\nchuyển sang định dạng opus hiện đã được bật", "mm_playerr": "phát lỗi: ", "mm_eabrt": "Việc phát nhạc đã bị huỷ", "mm_enet": "Kết nối Internet không ổn định", "mm_edec": "Tệp này dường như đã bị hỏng??", "mm_esupp": "Trình duyệt của bạn không nhận dạng được định dạng tệp này.", "mm_eunk": "Lỗi không xác định", "mm_e404": "Không thể phát âm thanh; lỗi 404: Không tìm thấy tệp.", "mm_e403": "Không thể phát âm thanh; lỗi 403: Từ chối truy cập.\n\nThử nhấn F5 để tải lại, có thể bạn đã đăng xuất", "mm_e415": "Không thể phát âm thanh; lỗi 415: Chuyển đổi tệp thất bại; kiểm tra nhật ký máy chủ.", //m "mm_e500": "Không thể phát âm thanh; lỗi 500: Kiểm tra nhật ký máy chủ.", "mm_e5xx": "Không thể phát âm thanh; lỗi máy chủ ", "mm_nof": "không tìm thấy thêm tệp âm thanh nào gần đó", "mm_prescan": "Đang tìm bài nhạc tiếp theo để phát...", "mm_scank": "Đã tìm thấy bài nhạc tiếp theo:", "mm_uncache": "đã xoá bộ nhớ đệm; tất cả bài nhạc sẽ được tải lại khi phát tiếp", "mm_hnf": "bài nhạc này không còn tồn tại nữa", "im_hnf": "hình ảnh này không còn tồn tại nữa", "f_empty": 'thư mục này trống', "f_chide": 'ẩn cột «{0}»\n\bạn có thế hiện lại nó trong tuỳ chọn cấu hình', "f_bigtxt": "tệp này nặng {0} MiB -- xác nhận xem dưới dạng văn bản?", "f_bigtxt2": "chỉ xem phần cuối của tệp? điều này cũng sẽ bật theo dõi, hiển thị các dòng văn bản mới được thêm vào theo thời gian thực", "fbd_more": '
          hiện {0} của {1} tệp; hiện {2} hoặc hiện tất cả
          ', "fbd_all": '
          đang hiện {0} của {1} tệp; hiện tất cả
          ', "f_anota": "chỉ {0} trong {1} tệp được chọn;\nđể chọn toàn bộ thư mục, trước tiên hãy kéo xuống cuối", "f_dls": 'những đường dẫn đến tệp trong thư mục này\nđã được chuyển thành đường dẫn tải trực tiếp', "f_dl_nd": 'bỏ qua thư mục (hãy dùng tải zip/tar thay thế):\n', //m "f_partial": "Để tải an toàn một tệp đang được tải lên, hãy bấm vào tệp có cùng tên nhưng *không* có phần mở rộng .PARTIAL. Hãy nhấn CANCEL hoặc Escape để thực hiện.\n\nNếu nhấn OK / Enter, cảnh báo sẽ bị bỏ qua và bạn sẽ tải tệp tạm .PARTIAL thay vào đó, gần như chắc chắn dẫn đến dữ liệu bị hỏng.", "ft_paste": "dán {0} mục$NPhím tắt: ctrl-V", "fr_eperm": "không thể đổi tên:\nbạn không có quyền “move” trong thư mục này", "fd_eperm": "không thể xóa:\nbạn không có quyền “delete” trong thư mục này", "fc_eperm": "không thể cắt:\nbạn không có quyền “move” trong thư mục này", "fp_eperm": "không thể dán:\nbạn không có quyền “write” trong thư mục này", "fr_emore": "hãy chọn ít nhất một mục để đổi tên", "fd_emore": "hãy chọn ít nhất một mục để xóa", "fc_emore": "hãy chọn ít nhất một mục để cắt", "fcp_emore": "hãy chọn ít nhất một mục để sao chép vào bảng nhớ tạm", "fs_sc": "chia sẻ thư mục hiện tại", "fs_ss": "chia sẻ các tệp đã chọn", "fs_just1d": "bạn không thể chọn nhiều hơn một thư mục,\nhoặc trộn tệp và thư mục trong cùng một lựa chọn", "fs_abrt": "❌ hủy", "fs_rand": "🎲 tên ngẫu nhiên", "fs_go": "✅ tạo liên kết chia sẻ", "fs_name": "tên", "fs_src": "nguồn", "fs_pwd": "mật khẩu", "fs_exp": "hết hạn", "fs_tmin": "phút", "fs_thrs": "giờ", "fs_tdays": "ngày", "fs_never": "vĩnh viễn", "fs_pname": "tên liên kết tùy chọn; sẽ dùng tên ngẫu nhiên nếu để trống", "fs_tsrc": "tệp hoặc thư mục cần chia sẻ", "fs_ppwd": "mật khẩu tùy chọn", "fs_w8": "đang tạo liên kết chia sẻ...", "fs_ok": "nhấn Enter/OK để chép vào bảng nhớ tạm\nnhấn ESC/Cancel để đóng", "frt_dec": "có thể sửa một số trường hợp tên tệp bị lỗi\">url-decode", "frt_rst": "khôi phục tên gốc\">↺ reset", "frt_abrt": "hủy và đóng cửa sổ này\">❌ cancel", "frb_apply": "ÁP DỤNG ĐỔI TÊN", "fr_adv": "đổi tên theo lô / metadata / pattern\">advanced", "fr_case": "regex phân biệt hoa thường\">case", "fr_win": "tên tương thích Windows; thay <>:"\\|?* bằng ký tự fullwidth tiếng Nhật\">win", "fr_slash": "thay / bằng ký tự khác để tránh tạo thư mục mới\">no /", "fr_re": "`regex áp dụng lên tên gốc; các nhóm bắt có thể được tham chiếu trong trường định dạng bên dưới như `(1)`, `(2)` ...", "fr_fmt": "`lấy cảm hứng từ foobar2000:$N`(title)` được thay bằng tên bài hát,$N`[(artist) - ](title)` bỏ qua phần trong ngoặc nếu artist trống,$N`$lpad((tn),2,0)` thêm số 0 để tracknumber đủ 2 chữ số", "fr_pdel": "xóa", "fr_pnew": "lưu dưới tên mới", "fr_pname": "nhập tên cho preset mới", "fr_aborted": "đã hủy", "fr_lold": "tên cũ", "fr_lnew": "tên mới", "fr_tags": "tag của các tệp đã chọn (chỉ xem, không chỉnh sửa):", "fr_busy": "đang đổi tên {0} mục...\n\n{1}", "fr_efail": "đổi tên thất bại:\n", "fr_nchg": "{0} tên mới đã bị chỉnh sửa do win và/hoặc no /\n\nTiếp tục với các tên đã chỉnh sửa?", "fd_ok": "hoàn tất xoá", "fd_err": "xoá gặp lỗi:\n", "fd_none": "không xóa được mục nào; có thể bị chặn bởi cấu hình máy chủ (xbd)?", "fd_busy": "đang xóa {0} mục...\n\n{1}", "fd_warn1": "XÓA {0} mục này?", "fd_warn2": "Cảnh báo cuối! Không thể hoàn tác. Xóa?", "fc_ok": "đã cắt {0} mục", "fc_warn": "đã cắt {0} mục\n\nnhưng: chỉ tab trình duyệt này có thể dán\n(vì lựa chọn quá lớn)", "fcc_ok": "đã sao chép {0} mục vào bảng nhớ tạm", "fcc_warn": "đã sao chép {0} mục vào bảng nhớ tạm\n\nnhưng: chỉ tab trình duyệt này có thể dán\n(vì lựa chọn quá lớn)", "fp_apply": "dùng các tên này", "fp_skip": "bỏ qua xung đột", //m "fp_ecut": "hãy cắt hoặc sao chép một số tệp / thư mục trước khi dán / di chuyển\n\nlưu ý: bạn có thể cắt / dán giữa các tab trình duyệt khác nhau", "fp_ename": "{0} mục không thể được di chuyển vào đây vì tên đã tồn tại. Hãy đặt tên mới bên dưới để tiếp tục, hoặc để trống (\"bỏ qua xung đột\") để bỏ qua:", //m "fcp_ename": "{0} mục không thể được sao chép vào đây vì tên đã tồn tại. Hãy đặt tên mới bên dưới để tiếp tục, hoặc để trống (\"bỏ qua xung đột\") để bỏ qua:", //m "fp_emore": "vẫn còn xung đột tên tệp cần xử lý", "fp_ok": "di chuyển OK", "fcp_ok": "sao chép OK", "fp_busy": "đang di chuyển {0} mục...\n\n{1}", "fcp_busy": "đang sao chép {0} mục...\n\n{1}", "fp_abrt": "đang hủy...", "fp_err": "di chuyển thất bại:\n", "fcp_err": "sao chép thất bại:\n", "fp_confirm": "di chuyển {0} mục này vào đây?", "fcp_confirm": "sao chép {0} mục này vào đây?", "fp_etab": "không đọc được bảng nhớ tạm từ tab trình duyệt khác", "fp_name": "đang tải lên tệp từ thiết bị. Hãy đặt tên cho tệp:", "fp_both_m": "
          chọn thao tác để dán
          Enter = Di chuyển {0} tệp từ «{1}»\nESC = Tải lên {2} tệp từ thiết bị", "fcp_both_m": "
          chọn thao tác để dán
          Enter = Sao chép {0} tệp từ «{1}»\nESC = Tải lên {2} tệp từ thiết bị", "fp_both_b": "Di chuyểnTải lên", "fcp_both_b": "Sao chépTải lên", "mk_noname": "hãy nhập tên vào ô bên trái trước khi thực hiện :p", "nmd_i1": "hãy thêm cả phần mở rộng tệp bạn muốn, ví dụ .md", "nmd_i2": "bạn chỉ có thể tạo tệp .{0} vì bạn không có quyền xóa", "tv_load": "Đang tải tài liệu văn bản:\n\n{0}\n\n{1}% ({2} / {3} MiB)", "tv_xe1": "không thể tải tệp văn bản:\n\nlỗi ", "tv_xe2": "404, không tìm thấy tệp", "tv_lst": "danh sách các tệp văn bản trong", "tvt_close": "quay lại chế độ xem thư mục$NPhím tắt: M (hoặc Esc)\">❌ close", "tvt_dl": "tải xuống tệp này$NPhím tắt: Y\">💾 download", "tvt_prev": "hiển thị tài liệu trước đó$NPhím tắt: I\">⬆ prev", "tvt_next": "hiển thị tài liệu kế tiếp$NPhím tắt: K\">⬇ next", "tvt_sel": "chọn tệp   (để cắt / sao chép / xóa / ...)$NPhím tắt: S\">sel", "tvt_j": "chuẩn hóa json$NPhím tắt: shift-J\">j", "tvt_edit": "mở tệp trong trình soạn thảo văn bản$NPhím tắt: E\">✏️ edit", "tvt_tail": "theo dõi thay đổi của tệp; hiển thị dòng mới theo thời gian thực\">📡 follow", "tvt_wrap": "ngắt dòng\">↵", "tvt_atail": "khóa cuộn ở cuối trang\">⚓", "tvt_ctail": "giải mã màu terminal (ansi escape codes)\">🌈", "tvt_ntail": "giới hạn scrollback (số byte văn bản được giữ trong bộ nhớ)", "m3u_add1": "đã thêm 1 bài vào danh sách phát m3u", "m3u_addn": "đã thêm {0} bài vào danh sách phát m3u", "m3u_clip": "danh sách phát m3u đã được chép vào bảng nhớ tạm\n\nbạn nên tạo một tệp văn bản mới tên bất kỳ.m3u rồi dán nội dung danh sách phát vào đó để có thể phát được", "gt_vau": "không hiện video, chỉ phát âm thanh\">🎧", "gt_msel": "bật chọn nhiều; ctrl-click để ghi đè$N$N<em>khi bật: nhấn đúp để mở tệp / thư mục</em>$N$NPhím tắt: S\">multiselect", "gt_crop": "cắt chính giữa ảnh\">crop", "gt_3x": "ảnh độ nét cao\">3x", "gt_zoom": "zoom", "gt_chop": "chop", "gt_sort": "sắp xếp theo", "gt_name": "tên", "gt_sz": "dung lượng", "gt_ts": "ngày", "gt_ext": "loại", "gt_c1": "rút ngắn tên tệp hơn (hiện ít hơn)", "gt_c2": "rút ngắn tên tệp ít hơn (hiện nhiều hơn)", "sm_w8": "đang tìm...", "sm_prev": "kết quả bên dưới là từ lần tìm trước:\n ", "sl_close": "đóng kết quả tìm kiếm", "sl_hits": "hiển thị {0} kết quả", "sl_moar": "tải thêm", "s_sz": "dung lượng", "s_dt": "ngày", "s_rd": "đường dẫn", "s_fn": "tên", "s_ta": "tag", "s_ua": "up@", "s_ad": "nâng cao", "s_s1": "tối thiểu MiB", "s_s2": "tối đa MiB", "s_d1": "ngày tối thiểu (iso8601)", "s_d2": "ngày tối đa (iso8601)", "s_u1": "tải lên sau", "s_u2": "và/hoặc trước", "s_r1": "đường dẫn chứa   (cách nhau bằng dấu cách)", "s_f1": "tên chứa   (thêm -nope để phủ định)", "s_t1": "tag chứa   (^=bắt đầu, kết thúc=$)", "s_a1": "thuộc tính metadata cụ thể", "md_eshow": "không thể tải", "md_off": "[📜readme] đã tắt trong [⚙️] -- tài liệu bị ẩn", "badreply": "Không thể phân tích phản hồi từ máy chủ", "xhr403": "403: Access denied\n\nhãy thử nhấn F5, có thể bạn đã bị đăng xuất", "xhr0": "không rõ (có thể mất kết nối với máy chủ hoặc máy chủ đang offline)", "cf_ok": "rất tiếc, lớp bảo vệ DD" + wah + "oS đã kích hoạt\n\nmọi thứ sẽ tiếp tục sau khoảng 30 giây\n\nnếu không có gì xảy ra, hãy nhấn F5 để tải lại trang", "tl_xe1": "không thể liệt kê thư mục con:\n\nlỗi ", "tl_xe2": "404: Không tìm thấy thư mục", "fl_xe1": "không thể liệt kê tệp trong thư mục:\n\nlỗi ", "fl_xe2": "404: Không tìm thấy thư mục", "fd_xe1": "không thể tạo thư mục con:\n\nlỗi ", "fd_xe2": "404: Không tìm thấy thư mục cha", "fsm_xe1": "không thể gửi tin nhắn:\n\nlỗi ", "fsm_xe2": "404: Không tìm thấy thư mục cha", "fu_xe1": "không tải được danh sách unpost từ máy chủ:\n\nlỗi ", "fu_xe2": "404: Không tìm thấy tệp??", "fz_tar": "file gnu-tar không nén (linux / mac)", "fz_pax": "file tar định dạng pax không nén (chậm hơn)", "fz_targz": "gnu-tar với nén gzip mức 3$N$Nthường rất chậm nên$Nhãy dùng tar không nén", "fz_tarxz": "gnu-tar với nén xz mức 1$N$Nthường rất chậm nên$Nhãy dùng tar không nén", "fz_zip8": "zip với tên tệp utf8 (có thể lỗi trên windows 7 và cũ hơn)", "fz_zipd": "zip với tên tệp cp437 truyền thống, dành cho phần mềm rất cũ", "fz_zipc": "cp437 với crc32 tính sớm,$Ndành cho MS-DOS PKZIP v2.04g (tháng 10/1993)$N(mất thời gian xử lý lâu hơn trước khi tải xuống bắt đầu)", "un_m1": "bạn có thể xóa các lần upload gần đây (hoặc hủy những mục chưa hoàn tất) bên dưới", "un_upd": "làm mới", "un_m4": "hoặc chia sẻ các tệp hiển thị bên dưới:", "un_ulist": "hiện", "un_ucopy": "chép", "un_flt": "bộ lọc tùy chọn:  đường dẫn phải chứa", "un_fclr": "xóa bộ lọc", "un_derr": "xóa unpost thất bại:\n", "un_f5": "có gì đó lỗi rồi, hãy thử tải lại trang hoặc nhấn F5", "un_uf5": "xin lỗi nhưng bạn cần tải lại trang (ví dụ nhấn F5 hoặc CTRL-R) trước khi có thể hủy upload này", "un_nou": "cảnh báo: máy chủ đang bận nên không thể hiển thị các upload chưa hoàn tất; hãy bấm “làm mới” sau một lúc", "un_noc": "cảnh báo: unpost cho các tệp đã upload xong không được bật hoặc không được phép trong cấu hình máy chủ", "un_max": "đang hiển thị 2000 tệp đầu tiên (hãy dùng bộ lọc)", "un_avail": "có thể xóa {0} tải lên gần đây
          {1} mục chưa hoàn tất có thể hủy", "un_m2": "sắp xếp theo thời gian tải lên; mới nhất ở trên:", "un_no1": "không có tải lên nào đủ mới", "un_no2": "không có tải lên nào đủ mới khớp với bộ lọc đó", "un_next": "xóa {0} tệp kế bên dưới", "un_abrt": "hủy", "un_del": "xóa", "un_m3": "đang tải danh sách tải lên gần đây...", "un_busy": "đang xóa {0} tệp...", "un_clip": "{0} liên kết đã chép vào bảng nhớ tạm", "u_https1": "bạn nên", "u_https2": "chuyển sang https", "u_https3": "để có hiệu suất tốt hơn", "u_ancient": "trình duyệt của bạn quá cũ; bạn có thể dùng bup thay thế", "u_nowork": "cần Firefox 53+, Chrome 57+ hoặc iOS 11+", "tail_2old": "cần Firefox 105+, Chrome 71+ hoặc iOS 14.5+", "u_nodrop": "trình duyệt của bạn quá cũ để dùng kéo thả khi tải lên", "u_notdir": "đây không phải thư mục\n\ntrình duyệt của bạn quá cũ,\nvui lòng thử dùng dragdrop", "u_uri": "để kéo thả ảnh từ cửa sổ trình duyệt khác,\nvui lòng thả lên nút tải lên lớn", "u_enpot": "chuyển sang giao diện đơn giản (có thể tăng tốc độ tải lên)", "u_depot": "chuyển sang giao diện đầy đủ (có thể giảm tốc độ tải lên)", "u_gotpot": "đang chuyển sang giao diện đơn giản để cải thiện tốc độ tải lên\n\nbạn có thể đổi lại bất kỳ lúc nào", "u_pott": "

          tệp:   {0} hoàn tất,   {1} lỗi,   {2} đang chạy,   {3} chờ

          ", "u_ever": "đây là trình tải lên cơ bản; up2k yêu cầu
          Chrome 21 // Firefox 13 // Edge 12 // Opera 12 // Safari 5.1 trở lên", "u_su2k": "đây là trình tải lên cơ bản; up2k tốt hơn", "u_uput": "tối ưu tốc độ (bỏ qua checksum)", "u_ewrite": "bạn không có quyền ghi vào thư mục này", "u_eread": "bạn không có quyền đọc thư mục này", "u_enoi": "tìm kiếm tệp chưa được bật trong cấu hình máy chủ", "u_enoow": "ghi đè không khả dụng; cần quyền Xóa", "u_badf": "Đã bỏ qua {0} tệp (trong tổng {1}) có thể do quyền hệ thống tệp:\n\n", "u_blankf": "{0} tệp (trong tổng {1}) trống; vẫn tải lên?\n\n", "u_applef": "{0} tệp (trong tổng {1}) có thể không cần thiết;\nNhấn OK/Enter để BỎ QUA,\nNhấn Cancel/ESC để giữ lại và tải lên:\n\n", "u_just1": "\nCó thể sẽ hoạt động tốt hơn nếu bạn chỉ chọn một tệp", "u_ff_many": "nếu bạn dùng Linux / macOS / Android thì số lượng tệp này có thể làm Firefox crash\nnếu xảy ra, vui lòng thử lại hoặc dùng Chrome", "u_up_life": "Tệp tải lên sẽ bị xóa khỏi máy chủ\n{0} sau khi hoàn tất", "u_asku": "tải {0} tệp này lên {1}", "u_unpt": "bạn có thể hoàn tác hoặc xóa lần tải lên này bằng biểu tượng ở góc trên bên trái 🧯", "u_bigtab": "sắp hiển thị {0} tệp\n\nviệc này có thể làm trình duyệt treo, bạn có chắc không?", "u_scan": "Đang quét tệp...", "u_dirstuck": "bộ lặp thư mục bị treo khi truy cập {0} mục sau; sẽ bỏ qua:", "u_etadone": "Hoàn tất ({0}, {1} tệp)", "u_etaprep": "(đang chuẩn bị tải lên)", "u_hashdone": "hash hoàn tất", "u_hashing": "hash", "u_hs": "đang bắt tay...", "u_started": "tệp đang được tải lên; xem biểu tượng [🚀]", "u_dupdefer": "bị trùng; sẽ xử lý sau khi hoàn tất các tệp khác", "u_actx": "nhấn vào dòng này để tránh giảm hiệu suất khi chuyển cửa sổ hoặc tab", "u_fixed": "OK;  đã sửa 👍", "u_cuerr": "không tải được phần {0}/{1};\ncó thể không nghiêm trọng, tiếp tục\n\ntệp: {2}", "u_cuerr2": "máy chủ từ chối phần tải {0}/{1};\nsẽ thử lại sau\n\ntệp: {2}\n\nlỗi ", "u_ehstmp": "sẽ thử lại; xem góc dưới bên phải", "u_ehsfin": "máy chủ từ chối yêu cầu hoàn tất tải lên; đang thử lại...", "u_ehssrch": "máy chủ từ chối yêu cầu tìm kiếm; đang thử lại...", "u_ehsinit": "máy chủ từ chối yêu cầu bắt đầu tải lên; đang thử lại...", "u_eneths": "lỗi mạng khi thực hiện bắt tay; đang thử lại...", "u_enethd": "lỗi mạng khi kiểm tra tồn tại mục tiêu; đang thử lại...", "u_cbusy": "chờ máy chủ cho phép tiếp tục sau sự cố mạng...", "u_ehsdf": "máy chủ hết dung lượng!\n\nsẽ tiếp tục thử trong trường hợp có ai đó giải phóng dung lượng", "u_emtleak1": "có dấu hiệu cho thấy trình duyệt có thể bị rò rỉ bộ nhớ;\nvui lòng", "u_emtleak2": " chuyển sang https (khuyến nghị) hoặc ", "u_emtleak3": " ", "u_emtleakc": "thử cách sau:\n
          • nhấn F5 để tải lại trang
          • sau đó tắt nút  mt  trong  ⚙️ cài đặt
          • và thử tải lại
          Tốc độ có thể chậm hơn một chút\nXin lỗi vì sự bất tiện\n\nPS: chrome v107 đã có bản sửa", "u_emtleakf": "thử cách sau:\n
          • nhấn F5 để tải lại trang
          • sau đó bật 🥔 (potato) trong giao diện tải lên
          • và thử lại
          \nPS: firefox hy vọng sẽ có bản sửa", "u_s404": "không tìm thấy trên máy chủ", "u_expl": "giải thích", "u_maxconn": "đa số trình duyệt giới hạn giá trị này ở mức 6, nhưng Firefox cho phép tăng bằng connections-per-server trong about:config", "u_tu": "

          CẢNH BÁO: turbo đang bật  client có thể không nhận diện và tiếp tục tải lại tệp chưa hoàn tất; xem tooltip nút turbo

          ", "u_ts": "

          CẢNH BÁO: turbo đang bật  kết quả tìm kiếm có thể không chính xác; xem tooltip nút turbo

          ", "u_turbo_c": "turbo bị tắt trong cấu hình máy chủ", "u_turbo_g": "đang tắt turbo vì bạn không có quyền liệt kê thư mục trong phân vùng này", "u_life_cfg": "tự xóa sau phút (hoặc giờ)", "u_life_est": "tệp sẽ bị xóa ---", "u_life_max": "thư mục này áp dụng\nthời gian tồn tại tối đa {0}", "u_unp_ok": "cho phép unpost trong {0}", "u_unp_ng": "không cho phép unpost", "ue_ro": "bạn chỉ có quyền đọc thư mục này\n\n", "ue_nl": "bạn chưa đăng nhập", "ue_la": "bạn đang đăng nhập với tài khoản \"{0}\"", "ue_sr": "bạn đang ở chế độ tìm kiếm tệp\n\nhãy chuyển sang chế độ tải lên bằng cách nhấn biểu tượng kính lúp 🔎 (bên cạnh nút SEARCH), rồi thử lại\n\nxin lỗi", "ue_ta": "hãy thử tải lên lại; giờ có thể được", "ue_ab": "tệp này đang được tải lên ở thư mục khác và cần hoàn tất trước khi có thể tải lên nơi khác.\n\nBạn có thể hủy và quên tiến trình ban đầu bằng biểu tượng ở góc trên bên trái 🧯", "ur_1uo": "OK: tệp đã tải lên thành công", "ur_auo": "OK: toàn bộ {0} tệp đã tải lên thành công", "ur_1so": "OK: đã tìm thấy tệp trên máy chủ", "ur_aso": "OK: đã tìm thấy toàn bộ {0} tệp trên máy chủ", "ur_1un": "Tải lên thất bại", "ur_aun": "{0} lần tải lên đều thất bại", "ur_1sn": "KHÔNG tìm thấy tệp trên máy chủ", "ur_asn": "{0} tệp KHÔNG tìm thấy trên máy chủ", "ur_um": "Hoàn tất\n{0} tải lên thành công,\n{1} tải lên thất bại", "ur_sm": "Hoàn tất\n{0} tệp tìm thấy trên máy chủ,\n{1} tệp KHÔNG tìm thấy", "rc_opn": "mở", //m "rc_ply": "phát", //m "rc_pla": "phát dưới dạng âm thanh", //m "rc_txt": "mở trong trình xem tệp", //m "rc_md": "mở trong trình soạn thảo văn bản", //m "rc_dl": "tải xuống", //m "rc_zip": "tải xuống dưới dạng gói nén", //m "rc_cpl": "sao chép liên kết", //m "rc_del": "xóa", //m "rc_cut": "cắt", //m "rc_cpy": "sao chép", //m "rc_pst": "dán", //m "rc_rnm": "đổi tên", //m "rc_nfo": "thư mục mới", //m "rc_nfi": "tệp mới", //m "rc_sal": "chọn tất cả", //m "rc_sin": "đảo ngược lựa chọn", //m "rc_shf": "chia sẻ thư mục này", //m "rc_shs": "chia sẻ lựa chọn", //m "lang_set": "tải lại trang để áp dụng thay đổi ngôn ngữ", "splash": { "a1": "tải lại", "b1": "xin chào khách   (bạn chưa đăng nhập)", "c1": "đăng xuất", "d1": "ghi lại ngăn xếp", "d2": "hiển thị trạng thái của tất cả các luồng đang hoạt động", "e1": "tải lại cấu hình", "e2": "tải lại các tệp cấu hình (tài khoản/ổ đĩa/cờ ổ đĩa),$Nvà quét lại tất cả các ổ đĩa e2ds$N$Nchú ý: bất kỳ thay đổi nào đối với cài đặt toàn cục$Ncần khởi động lại hoàn toàn để có hiệu lực", "f1": "bạn có thể duyệt:", "g1": "bạn có thể tải lên:", "cc1": "thứ khác:", "h1": "vô hiệu hoá k304", "i1": "bật k304", "j1": "bật k304 sẽ ngắt kết nối client của bạn trên mỗi HTTP 304, tùy chọn này có thể ngăn một số proxy bị lỗi kẹt (đột ngột không tải được trang), nhưng nó cũng sẽ làm mọi thứ chậm hơn", "k1": "đặt lại cài đặt client", "l1": "đăng nhập để có thêm:", "m1": "chào mừng trở lại,", "n1": "404 không tìm thấy  ┐( ´ -`)┌", "o1": 'hoặc có thể bạn không có quyền truy cập -- thử mật khẩu hoặc về trang chủ', "p1": "403 bị cấm  ~┻━┻", "q1": 'sử dụng mật khẩu hoặc về trang chủ', "r1": "về trang chủ", ".s1": "quét lại", "t1": "hành động", "u2": "thời gian kể từ lần ghi máy chủ cuối cùng$N( tải lên / đổi tên / ... )$N$N17d = 17 ngày$N1h23 = 1 giờ 23 phút$N4m56 = 4 phút 56 giây", "v1": "kết nối", "v2": "sử dụng máy chủ này như một ổ cứng cục bộ", "w1": "chuyển sang https", "x1": "đổi mật khẩu", "y1": "chỉnh sửa chia sẻ", "z1": "mở khóa chia sẻ này:", "ta1": "điền mật khẩu mới:", "ta2": "nhập lại mật khẩu mới:", "ta3": "mật khẩu không khớp, xin hãy thử lại", "nop": "LỖI: Mật khẩu không được để trống", //m "nou": "LỖI: Tên người dùng và/hoặc mật khẩu không được để trống", //m "aa1": "tệp đến", "ab1": "vô hiệu hóa no304", "ac1": "bật no304", "ad1": "bật no304 sẽ vô hiệu hóa tất cả bộ nhớ đệm; hãy thử tùy chọn này nếu k304 không đủ. Tùy chọn này sẽ làm lãng phí một lượng lớn lưu lượng mạng!", "ae1": "tải xuống đang hoạt động:", "af1": "hiển thị các tệp đã tải lên gần đây", "ag1": "hiển thị người dùng IdP đã biết", //m } }; ================================================ FILE: copyparty/web/ui.css ================================================ :root { --font-main: sans-serif; --font-serif: serif; --font-mono: 'scp'; --fg: #ccc; --fg-max: #fff; --bg-u2: #2b2b2b; --bg-u5: #444; } html.y { --fg: #222; --fg-max: #000; --bg-u2: #f7f7f7; --bg-u5: #ccc; } html.bz { --bg-u2: #202231; } @font-face { font-family: 'scp'; font-display: swap; src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(deps/scp.woff2) format('woff2'); } html { text-size-adjust: 100%; -webkit-text-size-adjust: 100%; touch-action: manipulation; } #tt, #toast { position: fixed; max-width: 34em; max-width: min(34em, 90%); max-width: min(34em, calc(100% - 7em)); color: #ddd; color: var(--fg); background: #333; background: var(--bg-u2); border: 0 solid #777; box-shadow: 0 .2em .5em #111; border-radius: .4em; z-index: 9001; } #tt { max-width: min(34em, calc(100% - 3.3em)); overflow: hidden; margin: .7em 0; padding: 0 1.3em; height: 0; opacity: .1; transition: opacity 0.14s, height 0.14s, padding 0.14s; } #toast { bottom: 5em; right: -1em; line-height: 1.5em; padding: 1em 1.3em; margin-left: 3em; border-width: .4em 0; overflow-wrap: break-word; transform: translateX(100%); transition: transform .4s cubic-bezier(.2, 1.2, .5, 1), right .4s cubic-bezier(.2, 1.2, .5, 1); text-shadow: 1px 1px 0 #000; color: #fff; } #toast.top { top: 2em; bottom: unset; } #toastt { position: absolute; height: 1px; top: 1px; right: 1px; left: 1px; animation: toastt var(--tmtime) 0.07s steps(var(--tmstep)) forwards; transform-origin: right; } @keyframes toastt { to {transform: scaleX(0)} } #toast a { color: inherit; text-shadow: inherit; background: rgba(0, 0, 0, 0.4); border-radius: .3em; padding: .2em .3em; } #toast a#toastc { display: inline-block; position: absolute; overflow: hidden; left: 0; width: 0; opacity: 0; padding: .3em 0; margin: -.3em 0 0 0; line-height: 1.3em; color: #000; border: none; outline: none; text-shadow: none; border-radius: .5em 0 0 .5em; transition: left .3s, width .3s, padding .3s, opacity .3s; } #toastb { max-height: 70vh; overflow-y: auto; padding: .1em; } #toast.scroll #toastb { overflow-y: scroll; margin-right: -1.2em; padding-right: .7em; } #toast.r #toastb { text-align: right; } #toast pre { margin: 0; } #toast.hide { display: none; } #toast.vis { right: 1.3em; transform: inherit; transform: initial; } #toast.vis #toastc { left: -2em; width: .4em; padding: .3em .8em; opacity: 1; } #toast.inf { background: #07a; border-color: #0be; } #toast.inf #toastc { background: #0be; } #toast.inf #toastt { background: #8ef; } #toast.ok { background: #380; border-color: #8e4; } #toast.ok #toastc { background: #8e4; } #toast.ok #toastt { background: #cf9; } #toast.warn { background: #960; border-color: #fc0; } #toast.warn #toastc { background: #fc0; } #toast.warn #toastt { background: #fe9; } #toast.err { background: #900; border-color: #d06; } #toast.err #toastc { background: #d06; } #toast.err #toastt { background: #f9c; } #toast code { padding: 0 .2em; background: rgba(0,0,0,0.2); } #tth { color: #fff; background: #111; font-size: .9em; padding: 0 .26em; line-height: .97em; border-radius: 1em; position: absolute; display: none; } #tth.act { display: block; z-index: 9001; } #tt.b { padding: 0 2em; border-radius: .5em; box-shadow: 0 .2em 1em #000; } #tt.show { padding: 1em 1.3em; border-width: .4em 0; height: auto; opacity: 1; } #tt.show.b { padding: 1.5em 2em; border-width: .5em 0; } .logue code, #modalc code, #tt code { color: #eee; color: var(--fg-max); background: #444; background: var(--bg-u5); padding: .1em .3em; border-radius: .3em; line-height: 1.7em; } #tt em { color: #f6a; } html.y #tt { border-color: #888 #000 #777 #000; } html.bz #tt { border-color: #3b3f58; } html.y #tt, html.y #toast { box-shadow: 0 .3em 1em rgba(0,0,0,0.4); } #modalc code { color: #060; background: transparent; border: 1px solid #ccc; } html.y #tt em { color: #d38; } html.y #tth { color: #000; background: #fff; } #cf_frame { position: fixed; z-index: 573; top: 3em; left: 50%; width: 40em; height: 30em; margin-left: -20.2em; border-radius: .4em; border: .4em solid var(--fg); box-shadow: 0 2em 4em 1em var(--bg-max); } #hkhelp, #modal { position: fixed; overflow: auto; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; z-index: 9001; background: rgba(64,64,64,0.6); } #modal>table { width: 100%; height: 100%; } #modal td { text-align: center; } #modalc { position: relative; display: inline-block; background: #f7f7f7; color: #333; text-shadow: none; text-align: left; margin: 3em; padding: 1em 1.1em; border-radius: .6em; box-shadow: 0 .3em 3em rgba(0,0,0,0.5); max-width: 50em; max-height: 30em; overflow-x: auto; overflow-y: scroll; } #modalc.yk { overflow-y: auto; } #modalc td { text-align: unset; padding: .2em; } @media (min-width: 40em) { #modalc { min-width: 30em; } } #modalc li { margin: 1em 0; } #modalc h6 { font-size: 1.3em; border-bottom: 1px solid #999; margin: 0; padding: .3em; text-align: center; } #modalc a { color: #07b; } #modalc .b64 { display: block; margin: .1em auto; width: 60%; height: 60%; background: #999; background: rgba(128,128,128,0.2); } #modalb { position: sticky; text-align: right; padding-top: 1em; bottom: 0; right: 0; } #modalb a { color: #000; background: #ccc; display: inline-block; border-radius: .3em; padding: .5em 1em; outline: none; border: none; } #modalb a:focus, #modalb a:hover { background: #06d; color: #fff; } #modalb a+a { margin-left: .5em; } #modali { display: block; background: #fff; color: #000; width: calc(100% - 1.25em); margin: 1em -.1em 0 -.1em; padding: .5em; outline: none; border: .25em solid #ccc; border-radius: .4em; } #modali:focus { border-color: #06d; } #repl_pre { max-width: 24em; } *:focus, *:focus+label, #pctl *:focus, .btn:focus { box-shadow: 0 .1em .2em #fc0 inset; outline: #fc0 solid .1em; border-radius: .2em; } html.y *:focus, html.y *:focus+label, html.y #pctl *:focus, html.y .btn:focus { box-shadow: 0 .1em .2em #037 inset; outline: #037 solid .1em; } input, button { font-family: var(--font-main), sans-serif; } input[type="submit"] { cursor: pointer; } input[type="text"]:focus, input:not([type]):focus, textarea:focus { box-shadow: 0 .1em .3em #fc0, 0 -.1em .3em #fc0; } html.y input[type="text"]:focus, html.y input:not([type]):focus, html.y textarea:focus { box-shadow: 0 .1em .3em #037, 0 -.1em .3em #037; } .mdo pre, .mdo code, .mdo a { color: #480; background: #f7f7f7; border: .07em solid #ddd; border-radius: .2em; padding: .1em .3em; margin: 0 .1em; } .mdo pre, .mdo code, .mdo code[class*="language-"], .mdo tt { font-family: 'scp', monospace, monospace; font-family: var(--font-mono), 'scp', monospace, monospace; white-space: pre-wrap; word-break: break-all; } .mdo code { font-size: .96em; } html.z .mdo a>code, html.y .mdo a>code { color: inherit; background: inherit; background: rgba(0, 0, 0, 0.2); padding-top: 0; padding-bottom: 0; border: none; } .mdo h1, .mdo h2 { line-height: 1.5em; } .mdo h1 { font-size: 1.7em; text-align: center; border: 1em solid #777; border-width: .05em 0; margin: 3em 0; } .mdo h2 { font-size: 1.5em; font-weight: normal; background: #f7f7f7; border-top: .07em solid #fff; border-bottom: .07em solid #bbb; border-radius: .5em .5em 0 0; padding-left: .4em; margin-top: 3em; } .mdo h3 { border-bottom: .1em solid #999; } .mdo h1 a, .mdo h3 a, .mdo h5 a, .mdo h2 a, .mdo h4 a, .mdo h6 a { color: inherit; display: block; background: none; border: none; padding: 0; margin: 0; } .mdo ul, .mdo ol { padding-left: 1em; } .mdo ul ul, .mdo ul ol, .mdo ol ul, .mdo ol ol { padding-left: 2em; border-left: .3em solid #ddd; } .mdo ul>li { margin: .7em 0; list-style-type: disc; } .mdo ol>li { margin: .7em 0 .7em 2em; } .mdo strong { color: #000; } .mdo p>em, .mdo li>em, .mdo td>em { color: #c50; padding: .1em; border-bottom: .1em solid #bbb; } .mdo blockquote { font-family: serif; font-family: var(--font-serif), serif; background: #f7f7f7; border: .07em dashed #ccc; padding: 0 2em; margin: 1em 0; } .mdo small { opacity: .8; } .mdo pre code { display: block; margin: 0 -.3em; padding: .4em .5em; line-height: 1.1em; } .mdo pre code:hover { background: #fec; color: #360; } .mdo table { border-collapse: collapse; margin: 1em 0; } .mdo th, .mdo td { padding: .2em .5em; border: .12em solid #aaa; } .mdo .mdth, .mdo .mdthl, .mdo .mdthr { margin: .5em .5em .5em 0; } .mdthl { float: left; } .mdthr { float: right; } hr { clear: both; } @media screen { .mdo { word-break: break-word; overflow-wrap: break-word; word-wrap: break-word; /*ie*/ } html.y .mdo a, .mdo a { color: #fff; background: #39b; text-decoration: none; padding: 0 .3em; border: none; border-bottom: .07em solid #079; } .mdo h1 { color: #fff; background: #444; font-weight: normal; border-top: .4em solid #fb0; border-bottom: .4em solid #777; border-radius: 0 1em 0 1em; margin: 3em 0 1em 0; padding: .5em 0; } .mdo h2 { color: #fff; background: #555; margin-top: 2em; border-bottom: .22em solid #999; border-top: none; } html.z .mdo a { background: #057; } html.z .mdo h1 a, html.z .mdo h4 a, html.z .mdo h2 a, html.z .mdo h5 a, html.z .mdo h3 a, html.z .mdo h6 a { color: inherit; background: none; } html.z .mdo pre, html.z .mdo code { color: #8c0; background: #1a1a1a; border: .07em solid #333; } html.z .mdo ul, html.z .mdo ol { border-color: #444; } html.z .mdo strong { color: #fff; } html.z .mdo p>em, html.z .mdo li>em, html.z .mdo td>em { color: #f94; border-color: #666; } html.z .mdo h1 { background: #383838; border-top: .4em solid #b80; border-bottom: .4em solid #4c4c4c; } html.bz .mdo h1 { background: #202231; border: 1px solid #2d2f45; border-width: 0 0 .4em 0; } html.z .mdo h2 { background: #444; border-bottom: .22em solid #555; } html.bz .mdo h2, html.bz .mdo h3 { background: transparent; border-color: #3b3f58; } html.z .mdo td, html.z .mdo th { border-color: #444; } html.z .mdo blockquote { background: #282828; border: .07em dashed #444; } } @media (prefers-reduced-motion) { #toast, #toast a#toastc, #tt { transition: none; } } ================================================ FILE: copyparty/web/up2k.js ================================================ "use strict"; var J_U2K = 1; (function () { var x = sread('nosubtle'); if (x === '0' || x === '1') nosubtle = parseInt(x); if ((nosubtle > 1 && !CHROME && !FIREFOX) || (nosubtle > 2 && !CHROME) || (CHROME && nosubtle > VCHROME) || !WebAssembly) nosubtle = 0; })(); function goto_up2k() { if (up2k === false) return goto('bup'); if (!up2k) return setTimeout(goto_up2k, 100); up2k.init_deps(); } // chrome requires https to use crypto.subtle, // usually it's undefined but some chromes throw on invoke var up2k = null, up2k_hooks = [], hws = [], hws_ok = 0, hws_ng = false, sha_js = WebAssembly ? 'hw' : 'ac', // ff53,c57,sa11 m = 'will use ' + sha_js + ' instead of native sha512 due to'; try { if (nosubtle) throw 'chickenbit'; var cf = crypto.subtle || crypto.webkitSubtle; cf.digest('SHA-512', new Uint8Array(1)).then( function (x) { console.log('sha-ok'); up2k = up2k_init(cf); }, function (x) { console.log(m, x); up2k = up2k_init(false); } ); } catch (ex) { console.log(m, ex); try { up2k = up2k_init(false); } catch (ex) { console.log('up2k init failed:', ex); toast.err(10, 'could not initialize up2k\n\n' + basenames(ex)); } } treectl.onscroll(); function up2k_flagbus() { var flag = { "id": Math.floor(Math.random() * 1024 * 1024 * 1023 * 2), "ch": new BroadcastChannel("up2k_flagbus"), "ours": false, "owner": null, "wants": null, "act": false, "last_tx": ["x", null] }; var dbg = function (who, msg) { console.log('flagbus(' + flag.id + '): [' + who + '] ' + msg); }; flag.ch.onmessage = function (e) { var who = e.data[0], what = e.data[1]; if (who == flag.id) { dbg(who, 'hi me (??)'); return; } flag.act = Date.now(); if (what == "want") { // lowest id wins, don't care if that's us if (who < flag.id) { dbg(who, 'wants (ack)'); flag.wants = [who, flag.act]; } else { dbg(who, 'wants (ign)'); } } else if (what == "have") { dbg(who, 'have'); flag.owner = [who, flag.act]; } else if (what == "give") { if (flag.owner && flag.owner[0] == who) { flag.owner = null; dbg(who, 'give (ok)'); } else { dbg(who, 'give, INVALID, ' + flag.owner); } } else if (what == "hi") { dbg(who, 'hi'); flag.ch.postMessage([flag.id, "hey"]); } else { dbg('?', e.data); } }; var tx = function (now, msg) { var td = now - flag.last_tx[1]; if (td > 500 || flag.last_tx[0] != msg) { dbg('*', 'tx ' + msg); flag.ch.postMessage([flag.id, msg]); flag.last_tx = [msg, now]; } }; var do_take = function (now) { tx(now, "have"); flag.owner = [flag.id, now]; flag.ours = true; }; var do_want = function (now) { tx(now, "want"); }; flag.take = function (now) { if (flag.ours) { do_take(now); return; } if (flag.owner && now - flag.owner[1] > 12000) { flag.owner = null; } if (flag.wants && now - flag.wants[1] > 12000) { flag.wants = null; } if (!flag.owner && !flag.wants) { do_take(now); return; } do_want(now); }; flag.give = function () { dbg('#', 'put give'); flag.ch.postMessage([flag.id, "give"]); flag.owner = null; flag.ours = false; }; flag.ch.postMessage([flag.id, 'hi']); return flag; } function U2pvis(act, btns, uc, st) { var r = this; r.act = act; r.ctr = { "ok": 0, "ng": 0, "bz": 0, "q": 0 }; r.tab = []; r.hq = {}; r.head = 0; r.tail = -1; r.wsz = 3; r.npotato = 99; r.modn = 0; r.modv = 0; r.mod0 = null; var markup = { '404': '' + L.utl_404 + '', 'ERROR': '' + L.utl_err + '', 'OS-error': '' + L.utl_oserr + '', 'found': '' + L.utl_found + '', 'defer': '' + L.utl_defer + '', 'YOLO': '' + L.utl_yolo + '', 'done': '' + L.utl_done + '', }; r.addfile = function (entry, sz, draw) { r.tab.push({ "hn": entry[0], "ht": entry[1], "hp": entry[2], "in": 'q', "nh": 0, //hashed "nd": 0, //done "cb": [], // bytes done in chunk "bt": sz, // bytes total "bd": 0, // bytes done "bd0": 0 // upload start }); r.ctr["q"]++; if (!draw) return; r.drawcard("q"); if (r.act == "q") { r.addrow(r.tab.length - 1); } if (r.act == "bz") { r.bzw(); } }; r.is_act = function (card) { if (uc.potato && !uc.fsearch) return false; if (r.act == "done") return card == "ok" || card == "ng"; return r.act == card; } r.seth = function (nfile, field, html) { var fo = r.tab[nfile]; field = ['hn', 'ht', 'hp'][field]; if (fo[field] === html) return; fo[field] = html; if (!r.is_act(fo.in)) return; var k = 'f' + nfile + '' + field.slice(1), obj = ebi(k); obj.innerHTML = field == 'ht' ? (markup[html] || html) : html; if (field == 'hp') { obj.style.color = ''; obj.style.background = ''; delete r.hq[nfile]; } }; r.setab = function (nfile, nblocks) { var t = []; for (var a = 0; a < nblocks; a++) t.push(0); r.tab[nfile].cb = t; }; r.setat = function (nfile, blocktab) { var fo = r.tab[nfile], bd = 0; for (var a = 0; a < blocktab.length; a++) bd += blocktab[a]; fo.bd = bd; fo.bd0 = bd; fo.cb = blocktab; }; r.perc = function (bd, bd0, sz, t0) { var td = Date.now() - t0, p = bd * 100.0 / sz, nb = bd - bd0, spd = nb / (td / 1000), eta = spd ? (sz - bd) / spd : 3599; return [p, s2ms(eta), spd / (1024 * 1024)]; }; r.hashed = function (fobj) { var fo = r.tab[fobj.n], nb = fo.bt * (++fo.nh / fo.cb.length), p = r.perc(nb, 0, fobj.size, fobj.t_hashing); fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MB/s'; if (!r.is_act(fo.in)) return; var o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0]; r.hq[fobj.n] = [fo.hp, '#fff', 'linear-gradient(90deg, #025, #06a ' + o1 + '%, #09d ' + o2 + '%, #222 ' + o3 + '%, #222 99%, #555)']; }; r.prog = function (fobj, nchunk, cbd) { var fo = r.tab[fobj.n], delta = cbd - fo.cb[nchunk]; fo.cb[nchunk] = cbd; fo.bd += delta; if (!fo.bd) return; var p = r.perc(fo.bd, fo.bd0, fo.bt, fobj.t_uploading); fo.hp = f2f(p[0], 2) + '%, ' + p[1] + ', ' + f2f(p[2], 2) + ' MB/s'; if (!r.is_act(fo.in)) return; var obj = ebi('f' + fobj.n + 'p'), o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0]; if (!obj) { var msg = [ "act", r.act, "in", fo.in, "is_act", r.is_act(fo.in), "head", r.head, "tail", r.tail, "nfile", fobj.n, "name", fobj.name, "sz", fobj.size, "bytesDelta", delta, "bytesDone", fo.bd, ], m2 = '', ds = QSA("#u2tab>tbody>tr>td:first-child>a:last-child"); for (var a = 0; a < msg.length; a += 2) m2 += msg[a] + '=' + msg[a + 1] + ', '; console.log(m2); for (var a = 0, aa = ds.length; a < aa; a++) { var id = ds[a].parentNode.getAttribute('id').slice(1, -1); console.log("dom %d/%d = [%s] in(%s) is_act(%s) %s", a, aa, id, r.tab[id].in, r.is_act(fo.in), ds[a].textContent); } for (var a = 0, aa = r.tab.length; a < aa; a++) if (r.is_act(r.tab[a].in)) console.log("tab %d/%d = sz %s", a, aa, r.tab[a].bt); throw new Error('see console'); } r.hq[fobj.n] = [fo.hp, '#fff', 'linear-gradient(90deg, #050, #270 ' + o1 + '%, #4b0 ' + o2 + '%, #222 ' + o3 + '%, #222 99%, #555)']; }; r.move = function (nfile, newcat) { var fo = r.tab[nfile], oldcat = fo.in, bz_act = r.act == "bz"; if (oldcat == newcat) return; fo.in = newcat; r.ctr[oldcat]--; r.ctr[newcat]++; while (st.car < r.tab.length && has(['ok', 'ng'], r.tab[st.car].in)) st.car++; r.drawcard(oldcat); r.drawcard(newcat); if (r.is_act(newcat)) { r.tail = Math.max(r.tail, nfile + 1); if (!ebi('f' + nfile)) r.addrow(nfile); } else if (r.is_act(oldcat)) { while (r.head < Math.min(r.tab.length, r.tail) && r.precard[r.tab[r.head].in]) r.head++; if (!bz_act) { qsr("#f" + nfile); } } else return; if (bz_act) r.bzw(); }; r.bzw = function () { var mod = 0, t0 = Date.now(), first = QS('#u2tab>tbody>tr:first-child'); if (!first) return; var last = QS('#u2tab>tbody>tr:last-child'); first = parseInt(first.getAttribute('id').slice(1)); last = parseInt(last.getAttribute('id').slice(1)); while (r.head - first > r.wsz) { qsr('#f' + (first++)); mod++; } while (last - r.tail < r.wsz && last < r.tab.length - 1) { var obj = ebi('f' + (++last)); if (!obj) { r.addrow(last); mod++; } } if (mod && r.modn < 200 && ebi('repl').offsetTop) { if (++r.modn >= 10) { if (r.modn == 10) r.mod0 = Date.now(); r.modv += Date.now() - t0; } if (r.modn >= 200) { var n = r.modn - 10, ipu = r.modv / n, spu = (Date.now() - r.mod0) / n, ir = spu / ipu; console.log('bzw:', f2f(ipu, 2), ' spu:', f2f(spu, 2), ' ir:', f2f(ir, 2), ' tab:', r.tab.length); // efficiency estimates; // ir: 5=16% 4=50%,30% 27=100% // ipu: 2.7=16% 2=30% 1.6=50% 1.8=100% (ng for big files) if (ipu >= 1.5 && ir <= 9 && r.tab.length >= 1000 && r.tab[Math.floor(r.tab.length / 3)].bt <= 1024 * 1024 * 4) r.go_potato(); } } }; r.potatolabels = function () { var ode = ebi('u2depotato'), oen = ebi('u2enpotato'); if (!ode) return; ode.style.display = uc.potato ? '' : 'none'; oen.style.display = uc.potato ? 'none' : ''; } r.potato = function () { ebi('u2tabw').style.minHeight = ''; QS('#u2cards a[act="bz"]').click(); timer[uc.potato ? "add" : "rm"](draw_potato); timer[uc.potato ? "rm" : "add"](apply_html); r.potatolabels(); }; r.go_potato = function () { r.go_potato = noop; var ode = mknod('div', 'u2depotato'), oen = mknod('div', 'u2enpotato'), u2f = ebi('u2foot'), btn = ebi('potato'); ode.innerHTML = L.u_depot; oen.innerHTML = L.u_enpot; if (sread('potato') === null) { btn.click(); toast.inf(30, L.u_gotpot); sdrop('potato'); } u2f.appendChild(ode); u2f.appendChild(oen); ode.onclick = oen.onclick = btn.onclick; r.potatolabels(); }; function draw_potato() { if (++r.npotato < 2) return; r.npotato = 0; var html = [L.u_pott.format(r.ctr.ok, r.ctr.ng, r.ctr.bz, r.ctr.q)]; while (r.head < r.tab.length && has(["ok", "ng"], r.tab[r.head].in)) r.head++; var act = null; if (r.head < r.tab.length) act = r.tab[r.head]; if (act) html.push("

          file {0} of {1} :   {2}   {3}

          \n
          {4}
          ".format( r.head + 1, r.tab.length, act.ht, act.hp, act.hn)); html = html.join('\n'); if (r.hpotato == html) return; r.hpotato = html; ebi('u2mu').innerHTML = html; } function apply_html() { var oq = {}, n = 0; for (var k in r.hq) { var o = ebi('f' + k + 'p'); if (!o) continue; oq[k] = o; n++; } if (!n) return; for (var k in oq) { var o = oq[k], v = r.hq[k]; o.innerHTML = v[0]; o.style.color = v[1]; o.style.background = v[2]; } r.hq = {}; } r.drawcard = function (cat) { var cards = QSA('#u2cards>a>span'); if (cat == "q") { cards[4].innerHTML = r.ctr[cat]; return; } if (cat == "bz") { cards[3].innerHTML = r.ctr[cat]; return; } cards[2].innerHTML = r.ctr["ok"] + r.ctr["ng"]; if (cat == "ng") { cards[1].innerHTML = r.ctr[cat]; } if (cat == "ok") { cards[0].innerHTML = r.ctr[cat]; } }; r.changecard = function (card) { r.act = card; r.precard = has(["ok", "ng", "done"], r.act) ? {} : r.act == "bz" ? { "ok": 1, "ng": 1 } : { "ok": 1, "ng": 1, "bz": 1 }; r.postcard = has(["ok", "ng", "done"], r.act) ? { "bz": 1, "q": 1 } : r.act == "bz" ? { "q": 1 } : {}; r.head = -1; r.tail = -1; var html = []; for (var a = 0; a < r.tab.length; a++) { var rt = r.tab[a].in; if (r.is_act(rt)) { html.push(r.genrow(a, true)); r.tail = a; if (r.head == -1) r.head = a; } } if (r.head == -1) { for (var a = 0; a < r.tab.length; a++) { var rt = r.tab[a].in; if (r.precard[rt]) { r.head = a + 1; r.tail = a; } else if (r.postcard[rt]) { r.head = a; r.tail = a - 1; break; } } } if (r.head < 0) r.head = 0; if (card == "bz") { for (var a = r.head - 1; a >= r.head - r.wsz && a >= 0; a--) { html.unshift(r.genrow(a, true).replace(/>/, ">a ")); } for (var a = r.tail + 1; a <= r.tail + r.wsz && a < r.tab.length; a++) { html.push(r.genrow(a, true).replace(/>/, ">b ")); } } var el = ebi('u2tab'); el.tBodies[0].innerHTML = html.join('\n'); el.className = (uc.fsearch ? 'srch ' : 'up ') + r.act; }; r.genrow = function (nfile, as_html) { var row = r.tab[nfile], td1 = '' + row.hn + td + 't">' + (markup[row.ht] || row.ht) + td + 'p" class="prog">' + row.hp + ''; if (as_html) return '' + ret + ''; var obj = mknod('tr', 'f' + nfile); obj.innerHTML = ret; return obj; }; r.addrow = function (nfile) { var tr = r.genrow(nfile); ebi('u2tab').tBodies[0].appendChild(tr); }; btns = QSA(btns + '>a[act]'); for (var a = 0; a < btns.length; a++) { btns[a].onclick = function (e) { ev(e); var newtab = this.getAttribute('act'); var go = function () { for (var b = 0; b < btns.length; b++) { btns[b].className = ( btns[b].getAttribute('act') == newtab) ? 'act' : ''; } r.changecard(newtab); } var nf = r.ctr[newtab]; if (nf === undefined) nf = r.ctr["ok"] + r.ctr["ng"]; if (nf < 9000) return go(); modal.confirm(L.u_bigtab.format(nf), go, null); }; } r.changecard(r.act); r.potato(); } function Donut(uc, st) { var r = this, el = null, psvg = null, tenstrobe = null, tstrober = null, strobes = [], o = 20 * 2 * Math.PI, optab = QS('#ops a[data-dest="up2k"]'); optab.setAttribute('ico', optab.textContent); function svg(v) { var ico = v !== undefined, bg = ico ? '#333' : 'transparent', fg = '#fff', fsz = 52, rc = 32; if (r.eta && (r.eta > 99 || (uc.fsearch ? st.time.hashing : st.time.uploading) < 20)) r.eta = null; if (r.eta) { if (r.eta < 10) { fg = '#fa0'; fsz = 72; } rc = 8; } return ( '\n' + (ico ? '\n' : '\n') + (r.eta ? ( '' + r.eta + '' ) : ( '' )) ); } function pos() { return uc.fsearch ? Math.max(st.bytes.hashed, st.bytes.finished) : st.bytes.inflight + st.bytes.finished; } r.on = function (ya) { r.fc = r.tc = r.dc = 99; r.eta = null; r.base = pos(); optab.innerHTML = ya ? svg() : optab.getAttribute('ico'); el = QS('#ops a .donut'); clearTimeout(tenstrobe); if (!ya) { favico.upd(); wintitle(); if (document.visibilityState == 'hidden') tenstrobe = setTimeout(r.enstrobe, 500); //debounce } }; r.do = function () { if (!el) return; var t = st.bytes.total - r.base, v = pos() - r.base, ofs = o - o * v / t; if (!uc.potato || ++r.dc >= 4) { el.style.strokeDashoffset = ofs; r.dc = 0; } if (++r.tc >= 10) { var s = r.eta === null ? 'paused' : r.eta > 60 ? shumantime(r.eta) : (r.eta + 's'); wintitle("{0}%, {1}, #{2}, ".format( f2f(v * 100 / t, 1), s, st.files.length - st.nfile.upload), true); r.tc = 0; } if (favico.txt) { if (++r.fc < 10 && r.eta && r.eta > 99) return; var s = svg(ofs); if (s == psvg || (r.eta === null && r.fc < 10)) return; favico.upd('', s); psvg = s; r.fc = 0; } }; r.enstrobe = function () { strobes = ['████████████████', '________________', '████████████████']; tstrober = setInterval(strobe, 300); if (uc.upsfx && actx && actx.state != 'suspended') sfx_nice(); // firefox may forget that filedrops are user-gestures so it can skip this: if (uc.upnag && Notification && Notification.permission == 'granted') new Notification(uc.nagtxt); } function strobe() { var txt = strobes.pop(); wintitle(txt, false); if (!txt) clearInterval(tstrober); } } function sfx_nice() { if (true) { var osc = actx.createOscillator(), gain = actx.createGain(), gg = gain.gain, ft = [660, 880, 440, 660, 880], ofs = 0; osc.connect(gain); gain.connect(actx.destination); var ct = actx.currentTime + 0.03; osc.type = 'triangle'; while (ft.length) osc.frequency.setTargetAtTime( ft.shift(), ct + (ofs += 0.05), 0.001); gg.value = 0.15; gg.setTargetAtTime(0.8, ct, 0.01); gg.setTargetAtTime(0.3, ct + 0.13, 0.01); gg.setTargetAtTime(0, ct + ofs + 0.05, 0.02); osc.start(); setTimeout(function () { osc.stop(); osc.disconnect(); gain.disconnect(); }, 500); } } function fsearch_explain(n) { if (n) return toast.inf(60, L.ue_ro + (acct == '*' ? L.ue_nl : L.ue_la).format(acct)); if (bcfg_get('fsearch', false)) return toast.inf(60, L.ue_sr); return toast.inf(60, L.ue_ta); } function up2k_init(subtle) { var r = { "tact": Date.now(), "init_deps": init_deps, "set_fsearch": set_fsearch, "gotallfiles": [gotallfiles] // hooks }; setTimeout(function () { if (WebAssembly && !hws.length) fetch(SR + '/.cpr/w/w.hash.js?_=' + TS); }, 1000); function showmodal(msg) { ebi('u2notbtn').innerHTML = msg; ebi('u2btn').style.display = 'none'; ebi('u2notbtn').style.display = 'block'; ebi('u2conf').style.opacity = '0.5'; } function unmodal() { ebi('u2notbtn').style.display = 'none'; ebi('u2conf').style.opacity = '1'; ebi('u2btn').style.display = ''; ebi('u2notbtn').innerHTML = ''; } var suggest_up2k = L.u_su2k; function got_deps() { return subtle || window.asmCrypto || window.hashwasm; } var loading_deps = false; function init_deps() { if (!loading_deps && !got_deps()) { var fn = 'sha512.' + sha_js + '.js', m = L.u_https1 + ' ' + L.u_https2 + ' ' + L.u_https3; showmodal('

          loading ' + fn + '

          '); import_js(SR + '/.cpr/w/deps/' + fn, unmodal); if (HTTPS) { // chrome<37 firefox<34 edge<12 opera<24 safari<7 m = L.u_ancient; setmsg(''); } qsr('#u2depmsg'); var o = mknod('div', 'u2depmsg'); o.innerHTML = nosubtle ? '' : m; ebi('u2foot').appendChild(o); } loading_deps = true; } if (perms.length && !has(perms, 'read') && has(perms, 'write')) goto('up2k'); function setmsg(msg, type) { if (msg !== undefined) { ebi('u2err').className = type; ebi('u2err').innerHTML = msg; } else { ebi('u2err').className = ''; ebi('u2err').innerHTML = ''; } if (msg == suggest_up2k) { ebi('u2yea').onclick = function (e) { ev(e); goto('up2k'); }; } } function un2k(msg) { setmsg(msg, 'err'); return false; } setmsg(suggest_up2k, 'msg'); var u2szs = u2sz.split(','), u2sz_min = parseInt(u2szs[0]), u2sz_tgt = parseInt(u2szs[1]), u2sz_max = parseInt(u2szs[2]); var parallel_uploads = ebi('nthread').value = icfg_get('nthread', u2j), stitch_tgt = ebi('u2szg').value = icfg_get('u2sz', u2sz_tgt), uc = {}, fdom_ctr = 0, biggest_file = 0; bcfg_bind(uc, 'rand', 'u2rand', false, null, false); bcfg_bind(uc, 'multitask', 'multitask', true, null, false); bcfg_bind(uc, 'potato', 'potato', false, set_potato, false); bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false); bcfg_bind(uc, 'umod', 'umod', false, null, false); bcfg_bind(uc, 'u2ts', 'u2ts', !u2ts.endsWith('u'), set_u2ts, false); bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false); bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg); bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo); bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null); bcfg_bind(uc, 'az', 'u2sort', u2sort.indexOf('n') + 1, set_u2sort); bcfg_bind(uc, 'hashw', 'hashw', !!WebAssembly && !(CHROME && MOBILE) && (!subtle || !CHROME || VCHROME > 136), set_hashw); bcfg_bind(uc, 'hwasm', 'nosubtle', nosubtle, set_nosubtle); bcfg_bind(uc, 'upnag', 'upnag', false, set_upnag); bcfg_bind(uc, 'upsfx', 'upsfx', false, set_upsfx); uc.ow = parseInt(sread('u2ow', ['0', '1', '2', '3']) || u2ow); uc.owt = ['🛡️', '🕒', '♻️', '⏭️']; function set_ow() { QS('label[for="u2ow"]').innerHTML = uc.owt[uc.ow]; ebi('u2ow').checked = true; //cosmetic } ebi('u2ow').onclick = function (e) { ev(e); if (++uc.ow > 3) uc.ow = 0; swrite('u2ow', uc.ow); set_ow(); if (uc.ow && uc.ow !== 3 && !has(perms, 'delete')) toast.warn(10, L.u_enoow, 'noow'); else if (toast.tag == 'noow') toast.hide(); }; set_ow(); var st = { "files": [], "nfile": { "hash": 0, "upload": 0 }, "seen": {}, "todo": { "head": [], "hash": [], "handshake": [], "upload": [] }, "busy": { "head": [], "hash": [], "handshake": [], "upload": [] }, "bytes": { "total": 0, "hashed": 0, "inflight": 0, "uploaded": 0, "finished": 0 }, "time": { "hashing": 0.01, "uploading": 0.01, "busy": 0.01 }, "eta": { "h": "", "u": "", "t": "" }, "etaw": { "h": [['', 0, 0, 0]], "u": [['', 0, 0, 0]], "t": [['', 0, 0, 0]] }, "etac": { "h": 0, "u": 0, "t": 0 }, "car": 0, "nre": 0, "slow_io": null, "oserr": false, "modn": 0, "modv": 0, "mod0": null }; function push_t(arr, t) { var sort = arr.length && arr[arr.length - 1].n > t.n; arr.push(t); if (sort) arr.sort(function (a, b) { return a.n < b.n ? -1 : 1; }); } var pvis = new U2pvis("bz", '#u2cards', uc, st), donut = new Donut(uc, st); r.ui = pvis; r.st = st; r.uc = uc; if (!window.File || !window.FileReader || !window.FileList || !File.prototype || !File.prototype.slice) return un2k(L.u_ever); var flag = false; apply_flag_cfg(); set_fsearch(); function nav() { start_actx(); var uf = function () { ebi('file' + fdom_ctr).click(); }, ud = function () { ebi('dir' + fdom_ctr).click(); }; // too buggy on chrome <= 72 var m = / Chrome\/([0-9]+)\./.exec(UA); if (m && parseInt(m[1]) < 73) return uf(); // phones dont support folder upload if (MOBILE) return uf(); modal.confirm(L.u_nav_m, uf, ud, null, L.u_nav_b); } ebi('u2btn').onclick = nav; var nenters = 0; function ondrag(e) { if (++nenters <= 0) nenters = 1; if (onover.call(this, e)) return true; var mup, up = QS('#up_zd'); var msr, sr = QS('#srch_zd'); if (!has(perms, 'write')) mup = L.u_ewrite; if (!has(perms, 'read')) msr = L.u_eread; if (!have_up2k_idx) msr = L.u_enoi; up.querySelector('span').textContent = mup || L.udt_drop; sr.querySelector('span').textContent = msr || L.udt_drop; clmod(up, 'err', mup); clmod(sr, 'err', msr); clmod(up, 'ok', !mup); clmod(sr, 'ok', !msr); ebi('up_dz').setAttribute('err', mup || ''); ebi('srch_dz').setAttribute('err', msr || ''); } function onoverb(e) { // zones are alive; disable cuo2duo branch document.body.ondragover = document.body.ondrop = null; return onover.call(this, e); } function onover(e) { return onovercmn(this, e, false); } function onoverbtn(e) { return onovercmn(this, e, true); } function onovercmn(self, e, btn) { try { var ok = false, dt = e.dataTransfer.types; for (var a = 0; a < dt.length; a++) if (dt[a] == 'Files') ok = true; else if (dt[a] == 'text/uri-list') { if (btn) { ok = true; if (toast.txt == L.u_uri) toast.hide(); } else return toast.inf(10, L.u_uri) || true; } if (!ok) return true; } catch (ex) { } ev(e); try { e.dataTransfer.dropEffect = 'copy'; e.dataTransfer.effectAllowed = 'copy'; } catch (ex) { document.body.ondragenter = document.body.ondragleave = document.body.ondragover = null; return modal.alert(L.u_nodrop); } if (btn) return; clmod(ebi('drops'), 'vis', 1); var v = self.getAttribute('v'); if (v) clmod(ebi(v), 'hl', 1); } function offdrag(e) { noope(e); var v = this.getAttribute('v'); if (v) clmod(ebi(v), 'hl'); if (--nenters <= 0) { clmod(ebi('drops'), 'vis'); clmod(ebi('up_dz'), 'hl'); clmod(ebi('srch_dz'), 'hl'); // cuo2duo: document.body.ondragover = onover; document.body.ondrop = gotfile; } } document.body.ondragenter = ondrag; document.body.ondragleave = offdrag; document.body.ondragover = onover; document.body.ondrop = gotfile; ebi('u2btn').ondrop = gotfile; ebi('u2btn').ondragover = onoverbtn; var drops = [ebi('up_dz'), ebi('srch_dz')]; for (var a = 0; a < 2; a++) { drops[a].ondragenter = ondrag; drops[a].ondragover = onoverb; drops[a].ondragleave = offdrag; drops[a].ondrop = gotfile; } ebi('drops').onclick = offdrag; // old ff function gotdir(e) { ev(e); var good_files = [], nil_files = [], bad_files = []; for (var a = 0, aa = e.target.files.length; a < aa; a++) { var fobj = e.target.files[a], name = fobj.webkitRelativePath, dst = good_files; try { if (!name) throw 1; if (fobj.size < 1) dst = nil_files; } catch (ex) { dst = bad_files; } dst.push([fobj, name]); } if (!good_files.length && bad_files.length) return toast.err(30, L.u_notdir); return read_dirs(null, [], [], good_files, nil_files, bad_files); } function gotfile(e) { ev(e); nenters = 0; offdrag.call(this); var dz = this && this.getAttribute('id'); if (!dz && e && e.clientY) // cuo2duo fallback dz = e.clientY < window.innerHeight / 2 ? 'up_dz' : 'srch_dz'; var err = this.getAttribute('err'); if (err) return modal.alert('sorry, ' + err); toast.inf(0, L.u_scan); if ((dz == 'up_dz' && uc.fsearch) || (dz == 'srch_dz' && !uc.fsearch)) tgl_fsearch(); if (!QS('#op_up2k.act')) goto('up2k'); var files, is_itemlist = false; if (e.dataTransfer) { if (e.dataTransfer.items) { files = e.dataTransfer.items; // DataTransferItemList is_itemlist = true; } else files = e.dataTransfer.files; // FileList } else files = e.target.files; if (!files || !files.length) return toast.err(0, 'no files selected??'); more_one_file(); var bad_files = [], nil_files = [], good_files = [], dirs = []; for (var a = 0; a < files.length; a++) { var fobj = files[a], dst = good_files; if (is_itemlist) { if (fobj.kind !== 'file' && fobj.type !== 'text/uri-list') continue; try { var wi = fobj.getAsEntry ? fobj.getAsEntry() : fobj.webkitGetAsEntry(); if (wi.isDirectory) { dirs.push(wi); continue; } } catch (ex) { } fobj = fobj.getAsFile(); if (!fobj) continue; } try { if (fobj.size < 1) dst = nil_files; } catch (ex) { dst = bad_files; } dst.push([fobj, fobj.name]); } start_actx(); // good enough for chrome; not firefox return read_dirs(null, [], dirs, good_files, nil_files, bad_files); } function rd_flatten(pf, dirs) { var ret = jcp(pf); for (var a = 0; a < dirs.length; a++) ret.push(dirs.fullPath || ''); ret.sort(); return ret; } var rd_missing_ref = []; function read_dirs(rd, pf, dirs, good, nil, bad, spins) { spins = spins || 0; if (++spins == 5) rd_missing_ref = rd_flatten(pf, dirs); if (spins == 200) { var missing = rd_flatten(pf, dirs), match = rd_missing_ref.length == missing.length, aa = match ? missing.length : 0; missing.sort(); for (var a = 0; a < aa; a++) if (rd_missing_ref[a] != missing[a]) match = false; if (match) { var msg = [L.u_dirstuck.format(missing.length) + '
            ']; for (var a = 0; a < Math.min(20, missing.length); a++) msg.push('
          • ' + esc(missing[a]) + '
          • '); return modal.alert(msg.join('') + '
          ', function () { read_dirs(rd, [], [], good, nil, bad, spins); }); } spins = 0; } if (!dirs.length) { if (!pf.length) // call first hook, pass list of remaining hooks to call return r.gotallfiles[0](good, nil, bad, r.gotallfiles.slice(1)); console.log("retry pf, " + pf.length); setTimeout(function () { read_dirs(rd, pf, dirs, good, nil, bad, spins); }, 50); return; } if (!rd) rd = dirs[0].createReader(); rd.readEntries(function (ents) { var ngot = 0; ents.forEach(function (dn) { if (dn.isDirectory) { dirs.push(dn); } else { var name = dn.fullPath; if (name.startsWith('/')) name = name.slice(1); pf.push(name); dn.file(function (fobj) { apop(pf, name); var dst = good; try { if (fobj.size < 1) dst = nil; } catch (ex) { dst = bad; } dst.push([fobj, name]); }); } ngot += 1; }); if (!ngot) { dirs.shift(); rd = null; } read_dirs(rd, pf, dirs, good, nil, bad, spins); }, function () { var dn = dirs[0], name = dn.fullPath; if (name.startsWith('/')) name = name.slice(1); bad.push([dn, name + '/']); read_dirs(null, pf, dirs.slice(1), good, nil, bad, spins); }); } function gotallfiles(good_files, nil_files, bad_files) { if (toast.txt == L.u_scan) toast.hide(); if (uc.fsearch && !uc.turbo) nil_files = []; var ntot = good_files.concat(nil_files, bad_files).length; if (bad_files.length) { var msg = L.u_badf.format(bad_files.length, ntot); for (var a = 0, aa = Math.min(20, bad_files.length); a < aa; a++) msg += '-- ' + esc(bad_files[a][1]) + '\n'; msg += L.u_just1; return modal.alert(msg, function () { start_actx(); gotallfiles(good_files, nil_files, []); }); } if (nil_files.length) { var msg = L.u_blankf.format(nil_files.length, ntot); for (var a = 0, aa = Math.min(20, nil_files.length); a < aa; a++) msg += '-- ' + esc(nil_files[a][1]) + '\n'; msg += L.u_just1; return modal.confirm(msg, function () { start_actx(); gotallfiles(good_files.concat(nil_files), [], []); }, function () { start_actx(); gotallfiles(good_files, [], []); }); } var fps = new Set(), pdp = ''; for (var a = 0; a < good_files.length; a++) { var fp = good_files[a][1], dp = vsplit(fp)[0]; fps.add(fp); if (pdp != dp) { pdp = dp; dp = dp.slice(0, -1); while (dp) { fps.add(dp); dp = vsplit(dp)[0].slice(0, -1); } } } var junk = [], rmi = []; for (var a = 0; a < good_files.length; a++) { var fn = good_files[a][1]; if (fn.indexOf("/.") < 0 && fn.indexOf("/__MACOS") < 0) continue; if (/\/__MACOS|\/\.(DS_Store|AppleDouble|LSOverride|DocumentRevisions-|fseventsd|Spotlight-V[0-9]|TemporaryItems|Trashes|VolumeIcon\.icns|com\.apple\.timemachine\.donotpresent|AppleDB|AppleDesktop|apdisk)/.exec(fn)) { junk.push(good_files[a]); rmi.push(a); continue; } if (fn.indexOf("/._") + 1 && fps.has(fn.replace("/._", "/")) && fn.split("/").pop().startsWith("._") && !has(rmi, a) ) { junk.push(good_files[a]); rmi.push(a); } } if (!junk.length) return gotallfiles2(good_files); junk.sort(); rmi.sort(function (a, b) { return a - b; }); var msg = L.u_applef.format(junk.length, good_files.length); for (var a = 0, aa = Math.min(1000, junk.length); a < aa; a++) msg += '-- ' + esc(junk[a][1]) + '\n'; return modal.confirm(msg, function () { for (var a = rmi.length - 1; a >= 0; a--) good_files.splice(rmi[a], 1); start_actx(); gotallfiles2(good_files); }, function () { start_actx(); gotallfiles2(good_files); }); } function gotallfiles2(good_files) { good_files.sort(function (a, b) { return a[1] < b[1] ? -1 : 1; }); var msg = []; if (lifetime) msg.push('' + L.u_up_life.format(lhumantime(st.lifetime || lifetime)) + '\n\n'); if (FIREFOX && good_files.length > 3000) msg.push(L.u_ff_many + "\n\n"); msg.push(L.u_asku.format(good_files.length, esc(uricom_dec(get_evpath()))) + '
            '); for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++) msg.push('
          • ' + esc(good_files[a][1]) + '
          • '); if (uc.ask_up && !uc.fsearch) return modal.confirm(msg.join('') + '
          ', function () { start_actx(); up_them(good_files); if (have_up2k_idx) toast.inf(15, L.u_unpt, L.u_unpt); }, null); up_them(good_files); } function up_them(good_files) { start_actx(); draw_turbo(); var evpath = get_evpath(), draw_each = good_files.length < 50; if (WebAssembly && !hws.length) { var nw = Math.min(navigator.hardwareConcurrency || 4, 16); if (CHROME) { // chrome-bug 383568268 // #124 nw = Math.max(1, (nw > 4 ? 4 : (nw - 1))); if (VCHROME < 137) nw = (subtle && !MOBILE && nw > 2) ? 2 : nw; } var x = sread('u2hashers') || window.u2hashers; if (x) { console.log('u2hashers is overriding default-value ' + nw); nw = parseInt(x); } for (var a = 0; a < nw; a++) hws.push(new Worker(SR + '/.cpr/w/w.hash.js?_=' + TS)); if (!subtle) for (var a = 0; a < hws.length; a++) hws[a].postMessage('nosubtle'); console.log(hws.length + " hashers"); } if (!uc.az) good_files.sort(function (a, b) { return a[0].size - b[0].size; }); for (var a = 0; a < good_files.length; a++) { var fobj = good_files[a][0], name = good_files[a][1], fdir = evpath, now = Date.now(), lmod = (uc.u2ts && fobj.lastModified) || 0, ofs = name.lastIndexOf('/') + 1; if (ofs) { fdir += url_enc(name.slice(0, ofs)); name = name.slice(ofs); } var entry = { "n": st.files.length, "t0": now, "fobj": fobj, "name": name, "size": fobj.size || 0, "lmod": lmod / 1000, "purl": fdir, "done": false, "bytes_uploaded": 0, "hash": [] }, key = name + '\n' + entry.size + '\n' + lmod + '\n' + uc.fsearch; if (uc.fsearch) entry.srch = 1; else if (uc.rand) { entry.rand = true; entry.name = 'a\n' + entry.name; } else if (uc.umod) entry.umod = true; if (biggest_file < entry.size) biggest_file = entry.size; try { if (st.seen[fdir][key]) continue; } catch (ex) { st.seen[fdir] = {}; } st.seen[fdir][key] = 1; pvis.addfile([ uc.fsearch ? esc(entry.name) : linksplit( entry.purl + uricom_enc(entry.name), window.up_site).join(' / '), '📐 ' + L.u_hashing, '' ], entry.size, draw_each); st.bytes.total += entry.size; st.files.push(entry); if (uc.turbo) push_t(st.todo.head, entry); else push_t(st.todo.hash, entry); } if (!draw_each) { pvis.drawcard("q"); pvis.changecard(pvis.act); } ebi('u2tabw').className = 'ye'; setTimeout(function () { if (!actx || actx.state != 'suspended' || toast.visible) return; toast.warn(30, "
          " + L.u_actx + "
          "); }, 500); } function more_one_file() { fdom_ctr++; var elm = mknod('div'); elm.innerHTML = ( '' + '' ).format(fdom_ctr); ebi('u2form').appendChild(elm); ebi('file' + fdom_ctr).onchange = gotfile; ebi('dir' + fdom_ctr).onchange = gotdir; } more_one_file(); function linklist() { var ret = [], base = (window.up_site || location.origin).replace(/\/$/, ''); for (var a = 0; a < st.files.length; a++) { var t = st.files[a], url = t.purl + uricom_enc(t.name); if (t.fk) url += '?k=' + t.fk; ret.push(base + url); } return ret.join('\r\n'); } ebi('luplinks').onclick = function (e) { ev(e); modal.alert(linklist()); }; ebi('cuplinks').onclick = function (e) { ev(e); var txt = linklist(); cliptxt(txt + '\n', function () { toast.inf(5, L.un_clip.format(txt.split('\n').length)); }); }; var etaref = 0, etaskip = 0, utw_minh = 0, utw_read = 0, utw_card = 0; function etafun() { var nhash = st.busy.head.length + st.busy.hash.length + st.todo.head.length + st.todo.hash.length, nsend = st.busy.upload.length + st.todo.upload.length, now = Date.now(), td = (now - (etaref || now)) / 1000.0; etaref = now; if (td > 1.2) td = 0.05; //ebi('acc_info').innerHTML = humantime(st.time.busy) + ' ' + f2f(now / 1000, 1); if (utw_card != pvis.act) { utw_card = pvis.act; utw_read = 9001; ebi('u2tabw').style.minHeight = '0px'; } if (++utw_read >= 20) { utw_read = 0; utw_minh = parseInt(ebi('u2tabw').style.minHeight || '0'); } var minh = QS('#op_up2k.act') && st.is_busy ? Math.max(utw_minh, ebi('u2tab').offsetHeight + 32) : 0; if (utw_minh < minh || !utw_minh) { utw_minh = minh; ebi('u2tabw').style.minHeight = utw_minh + 'px'; } if (!nhash) { var h = L.u_etadone.format(humansize(st.bytes.hashed), pvis.ctr.ok + pvis.ctr.ng); if (st.eta.h !== h) { st.eta.h = ebi('u2etah').innerHTML = h; console.log('{0} hash, {1} up, {2} busy'.format( f2f(st.time.hashing, 1), f2f(st.time.uploading, 1), f2f(st.time.busy, 1))); } } if (!nsend && !nhash) { var h = L.u_etadone.format(humansize(st.bytes.uploaded), pvis.ctr.ok + pvis.ctr.ng); if (st.eta.u !== h) st.eta.u = ebi('u2etau').innerHTML = h; if (st.eta.t !== h) st.eta.t = ebi('u2etat').innerHTML = h; } if (!st.busy.hash.length && !hashing_permitted()) nhash = 0; if (!parallel_uploads || !(nhash + nsend) || (flag && flag.owner && !flag.ours)) return; var t = []; if (nhash) { st.time.hashing += td; t.push(['u2etah', st.bytes.hashed, st.bytes.hashed, st.time.hashing]); if (uc.fsearch) { st.time.busy += td; t.push(['u2etat', st.bytes.hashed, st.bytes.hashed, st.time.hashing]); } } var b_up = st.bytes.inflight + st.bytes.uploaded, b_fin = st.bytes.inflight + st.bytes.finished; if (nsend) { st.time.uploading += td; t.push(['u2etau', b_up, b_fin, st.time.uploading]); } if ((nhash || nsend) && !uc.fsearch) { if (!b_fin) { ebi('u2etat').innerHTML = L.u_etaprep; } else { st.time.busy += td; t.push(['u2etat', b_fin, b_fin, st.time.busy]); } } for (var a = 0; a < t.length; a++) { var hid = t[a][0], eid = hid.slice(-1), etaw = st.etaw[eid]; if (st.etac[eid] > 100) { // num chunks st.etac[eid] = 0; etaw.push(jcp(t[a])); if (etaw.length > 5) etaw.shift(); } var h = etaw[0], rem = st.bytes.total - t[a][2], bps = (t[a][1] - h[1]) / Math.max(0.1, t[a][3] - h[3]), eta = Math.floor(rem / bps); if (t[a][1] < 1024 || t[a][3] < 0.1) { ebi(hid).innerHTML = L.u_etaprep; continue; } donut.eta = eta; st.eta[eid] = '{0}, {1}/s, {2}'.format( humansize(rem), humansize(bps, 1), humantime(eta)); if (!etaskip) ebi(hid).innerHTML = st.eta[eid]; } if (++etaskip > 2) etaskip = 0; } function got_oserr() { if (!hws.length || !uc.hashw || st.oserr) return; st.oserr = true; var msg = HTTPS ? L.u_emtleak3 : L.u_emtleak2.format((location + '').replace(':', 's:')); modal.alert(L.u_emtleak1 + msg + (CHROME ? L.u_emtleakc : FIREFOX ? L.u_emtleakf : '')); } ///// //// /// actuator // function handshakes_permitted() { if (!st.todo.handshake.length) return true; var t = st.todo.handshake[0], cd = t.cooldown; if (cd && cd > Date.now()) return false; // keepalive or verify if (t.keepalive || t.t_uploaded) return true; if (parallel_uploads < st.busy.handshake.length) return false; if (t.n - st.car > Math.max(8, parallel_uploads)) // prevent runahead from a stuck upload (slow server hdd) return false; if ((uc.multitask ? parallel_uploads : 0) < st.todo.upload.length + st.busy.upload.length) return false; return true; } function hashing_permitted() { if (!parallel_uploads) return false; var nhs = st.todo.handshake.length + st.busy.handshake.length, nup = st.todo.upload.length + st.busy.upload.length; if (uc.multitask) { if (nhs + nup < parallel_uploads) return true; if (!uc.az) return nhs < 2; var ahead = st.bytes.hashed - st.bytes.finished, nmax = ahead < biggest_file / 8 ? 32 : 16; return ahead < biggest_file && nhs < nmax; } return handshakes_permitted() && 0 == nhs + nup; } var tasker = (function () { var running = false; var defer = function () { running = false; } var taskerd = function () { if (running) return; if (crashed || !got_deps()) return defer(); running = true; while (true) { var now = Date.now(), blocktime = now - r.tact, was_busy = !!st.is_busy, is_busy = !!( // gzip take the wheel st.car < st.files.length || st.busy.hash.length || st.todo.hash.length || st.busy.handshake.length || st.todo.handshake.length || st.busy.upload.length || st.todo.upload.length || st.busy.head.length || st.todo.head.length); if (blocktime > 2500) console.log('main thread blocked for ' + blocktime); r.tact = now; if (was_busy && !is_busy) { var nre = 0, nf = 0; for (var a = 0; a < st.files.length; a++) if (st.files[a].want_recheck) nre++; console.log('nre', nre, 'st', st.nre); if (st.nre != nre) { st.nre = nre; nf = st.files.length; } for (var a = 0; a < nf; a++) { var t = st.files[a]; if (t.want_recheck) { t.rechecks++; t.want_recheck = false; push_t(st.todo.handshake, t); } } is_busy = !!st.todo.handshake.length; try { if (!is_busy && !uc.fsearch && !msel.getsel().length && (!mp.au || mp.au.paused)) treectl.goto(); } catch (ex) { } } if (was_busy != is_busy) { st.is_busy = is_busy; window[(is_busy ? "add" : "remove") + "EventListener"]("beforeunload", warn_uploader_busy); donut.on(is_busy); if (!is_busy) { uptoast(); //throw console.hist.join('\n'); } else { timer.add(donut.do); timer.add(etafun, false); ebi('u2etas').style.textAlign = 'left'; } etafun(); } if (flag) { if (is_busy) { flag.take(now); ebi('u2flagblock').style.display = flag.ours ? '' : 'block'; if (!flag.ours) return defer(); } else if (flag.ours) { flag.give(); } } if (st.bytes.inflight && (st.bytes.inflight < 0 || !st.busy.upload.length)) { console.log('insane inflight ' + st.bytes.inflight); st.bytes.inflight = 0; } var mou_ikkai = false; var nprev = -1; for (var a = 0; a < st.todo.upload.length; a++) { var nf = st.todo.upload[a].nfile; if (nprev == nf) continue; nprev = nf; var t = st.files[nf]; if (now - t.t_busied > 1000 * 30 && now - t.t_handshake > 1000 * (21600 - 1800) ) { apop(st.todo.handshake, t); st.todo.handshake.unshift(t); t.keepalive = true; } } if (st.todo.head.length && st.busy.head.length < parallel_uploads && (!is_busy || st.todo.head[0].n - st.car < parallel_uploads * 2)) { exec_head(); mou_ikkai = true; } if (handshakes_permitted() && st.todo.handshake.length) { exec_handshake(); mou_ikkai = true; } if (st.todo.upload.length && st.busy.upload.length < parallel_uploads && can_upload_next()) { exec_upload(); mou_ikkai = true; } if (hashing_permitted() && st.todo.hash.length && !st.busy.hash.length) { exec_hash(); mou_ikkai = true; } if (is_busy && st.modn < 100) { var t0 = Date.now() + (ebi('repl').offsetTop ? 0 : 0); if (++st.modn >= 10) { if (st.modn == 10) st.mod0 = Date.now(); st.modv += Date.now() - t0; } if (st.modn >= 100) { var n = st.modn - 10, ipu = st.modv / n, spu = (Date.now() - st.mod0) / n, ir = spu / ipu; console.log('tsk:', f2f(ipu, 2), ' spu:', f2f(spu, 2), ' ir:', f2f(ir, 2)); // efficiency estimates; // ir: 8=16% 11=60% 16=90% 24=100% // ipu: 1=40% .8=60% .3=100% if (ipu >= 0.5 && ir <= 15) pvis.go_potato(); } } if (!mou_ikkai || crashed) return defer(); } } timer.add(taskerd, true); return taskerd; })(); function uptoast() { if (st.busy.handshake.length) return; for (var a = 0; a < st.files.length; a++) { var t = st.files[a]; if (t.want_recheck && t.rechecks < 999) return; } var sr = uc.fsearch, ok = pvis.ctr.ok, ng = pvis.ctr.ng, spd = Math.floor(st.bytes.finished / st.time.busy), suf = '\n\n{0} @ {1}/s'.format(shumantime(st.time.busy), humansize(spd)), t = uc.ask_up ? 0 : 10; console.log('toast', ok, ng); if (ok && ng) toast.warn(t, uc.nagtxt = (sr ? L.ur_sm : L.ur_um).format(ok, ng) + suf); else if (ok > 1) toast.ok(t, uc.nagtxt = (sr ? L.ur_aso : L.ur_auo).format(ok) + suf); else if (ok) toast.ok(t, uc.nagtxt = (sr ? L.ur_1so : L.ur_1uo) + suf); else if (ng > 1) toast.err(t, uc.nagtxt = (sr ? L.ur_asn : L.ur_aun).format(ng) + suf); else if (ng) toast.err(t, uc.nagtxt = (sr ? L.ur_1sn : L.ur_1un) + suf); timer.rm(etafun); timer.rm(donut.do); ebi('u2tabw').style.minHeight = '0px'; utw_minh = 0; if (pvis.act == 'bz') pvis.changecard('bz'); var n = st.files.length - 1, f = n >= 0 && st.files[n]; if (f && !f.srch) { var xhr = new XHR(), ct = 'application/x-www-form-urlencoded;charset=UTF-8'; xhr.open('POST', f.purl, true); xhr.setRequestHeader('Content-Type', ct); if (xhr.overrideMimeType) xhr.overrideMimeType('Content-Type', ct); xhr.send('msg=upload-queue-empty;' + uricom_enc(f.name)); } } function chill(t) { var now = Date.now(); if ((t.coolmul || 0) < 5 || now - t.cooldown < t.coolmul * 700) t.coolmul = Math.min((t.coolmul || 0.5) * 2, 32); var cd = now + 1000 * (t.coolmul + Math.random() * 4 + 2); t.cooldown = Math.floor(Math.max(cd, t.cooldown || 1)); return t; } ///// //// /// hashing // // https://gist.github.com/jonleighton/958841 function buf2b64(arrayBuffer) { var base64 = '', cset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', src = new Uint8Array(arrayBuffer), nbytes = src.byteLength, byteRem = nbytes % 3, mainLen = nbytes - byteRem, a, b, c, d, chunk; for (var i = 0; i < mainLen; i = i + 3) { chunk = (src[i] << 16) | (src[i + 1] << 8) | src[i + 2]; // create 8*3=24bit segment then split into 6bit segments a = (chunk & 16515072) >> 18; // (2^6 - 1) << 18 b = (chunk & 258048) >> 12; // (2^6 - 1) << 12 c = (chunk & 4032) >> 6; // (2^6 - 1) << 6 d = chunk & 63; // 2^6 - 1 // Convert the raw binary segments to the appropriate ASCII encoding base64 += cset[a] + cset[b] + cset[c] + cset[d]; } if (byteRem == 1) { chunk = src[mainLen]; a = (chunk & 252) >> 2; // (2^6 - 1) << 2 b = (chunk & 3) << 4; // 2^2 - 1 (zero 4 LSB) base64 += cset[a] + cset[b];//+ '=='; } else if (byteRem == 2) { chunk = (src[mainLen] << 8) | src[mainLen + 1]; a = (chunk & 64512) >> 10; // (2^6 - 1) << 10 b = (chunk & 1008) >> 4; // (2^6 - 1) << 4 c = (chunk & 15) << 2; // 2^4 - 1 (zero 2 LSB) base64 += cset[a] + cset[b] + cset[c];//+ '='; } return base64; } function hex2u8(txt) { return new Uint8Array(txt.match(/.{2}/g).map(function (b) { return parseInt(b, 16); })); } function get_chunksize(filesize) { var chunksize = 1024 * 1024, stepsize = 512 * 1024; while (true) { for (var mul = 1; mul <= 2; mul++) { var nchunks = Math.ceil(filesize / chunksize); if (nchunks <= 256 || (chunksize >= 32 * 1024 * 1024 && nchunks <= 4096)) return chunksize; chunksize += stepsize; stepsize *= mul; } } } function exec_hash() { var t = st.todo.hash.shift(); if (!t.size) return st.todo.handshake.push(t); st.busy.hash.push(t); st.nfile.hash = t.n; t.t_hashing = Date.now(); var bpend = 0, nchunk = 0, chunksize = get_chunksize(t.size), nchunks = Math.ceil(t.size / chunksize), csz_mib = chunksize / 1048576, tread = t.t_hashing, cache_buf = null, cache_car = 0, cache_cdr = 0, hashers = 0, hashtab = {}; // resolving subtle.digest w/o worker takes 1sec on blur if the actx hack breaks var use_workers = hws.length && !hws_ng && uc.hashw && (nchunks > 1 || document.visibilityState == 'hidden'), hash_par = (!subtle && !use_workers) ? 0 : csz_mib < 48 ? 2 : csz_mib < 96 ? 1 : 0; pvis.setab(t.n, nchunks); pvis.move(t.n, 'bz'); if (use_workers) return wexec_hash(t, chunksize, nchunks); var segm_next = function () { if (nchunk >= nchunks || bpend) return false; var nch = nchunk++, car = nch * chunksize, cdr = Math.min(chunksize + car, t.size); st.bytes.hashed += cdr - car; st.etac.h++; if (MOBILE && CHROME && st.slow_io === null && nch == 1 && cdr - car >= 1024 * 512) { var spd = Math.floor((cdr - car) / (Date.now() + 1 - tread)); st.slow_io = spd < 40 * 1024; console.log('spd {0}, slow: {1}'.format(spd, st.slow_io)); } if (cdr <= cache_cdr && car >= cache_car) { try { var ofs = car - cache_car, ofs2 = ofs + (cdr - car), buf = cache_buf.subarray(ofs, ofs2); hash_calc(nch, buf); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); } return; } var reader = new FileReader(), fr_cdr = cdr; if (st.slow_io) { var step = cdr - car, tgt = 48 * 1048576; while (step && fr_cdr - car < tgt) fr_cdr += step; if (fr_cdr - car > tgt && fr_cdr > cdr) fr_cdr -= step; if (fr_cdr > t.size) fr_cdr = t.size; } var orz = function (e) { bpend = 0; var buf = e.target.result; if (fr_cdr > cdr) { cache_buf = new Uint8Array(buf); cache_car = car; cache_cdr = fr_cdr; buf = cache_buf.subarray(0, cdr - car); } if (hashers < hash_par) segm_next(); hash_calc(nch, buf); }; reader.onload = function (e) { try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); } }; reader.onerror = function () { var err = esc('' + reader.error), handled = false; if (err.indexOf('NotReadableError') !== -1 || // win10-chrome defender err.indexOf('NotFoundError') !== -1 // macos-firefox permissions ) { pvis.seth(t.n, 1, 'OS-error'); pvis.seth(t.n, 2, err + ' @ ' + car); console.log('OS-error', reader.error, '@', car); handled = true; got_oserr(); } if (handled) { pvis.move(t.n, 'ng'); apop(st.busy.hash, t); st.bytes.finished += t.size; return; } toast.err(0, 'y o u b r o k e i t\nfile: ' + esc(t.name + '') + '\nerror: ' + err); }; bpend = 1; tread = Date.now(); reader.readAsArrayBuffer(t.fobj.slice(car, fr_cdr)); return true; }; var hash_calc = function (nch, buf) { hashers++; var orz = function (hashbuf) { var hslice = new Uint8Array(hashbuf).subarray(0, 33), b64str = buf2b64(hslice); hashers--; hashtab[nch] = b64str; t.hash.push(nch); pvis.hashed(t); if (t.hash.length < nchunks) return segm_next(); t.hash = []; for (var a = 0; a < nchunks; a++) t.hash.push(hashtab[a]); t.t_hashed = Date.now(); pvis.seth(t.n, 2, L.u_hashdone); pvis.seth(t.n, 1, '📦 wait'); apop(st.busy.hash, t); st.todo.handshake.push(t); tasker(); }; var hash_done = function (hashbuf) { try { orz(hashbuf); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); } }; if (subtle) subtle.digest('SHA-512', buf).then(hash_done); else setTimeout(function () { var u8buf = new Uint8Array(buf); if (sha_js == 'hw') { hashwasm.sha512(u8buf).then(function (v) { hash_done(hex2u8(v)) }); } else { var hasher = new asmCrypto.Sha512(); hasher.process(u8buf); hasher.finish(); hash_done(hasher.result); } }, 1); }; segm_next(); } function wexec_hash(t, chunksize, nchunks) { var nchunk = 0, reading = 0, max_readers = 1, opt_readers = 2, failed = false, free = [], busy = {}, nbusy = 0, init = 0, ninit = 0, hashtab = {}, mem = (MOBILE ? 128 : 256) * 1024 * 1024; if (!hws_ok) init = setInterval(function() { if (ninit < hws_ok) { ninit = hws_ok; return toast.inf(10, 'initializing webworkers ({0}/{1})'.format(hws_ok, hws.length), "iwwt"); } clearInterval(init); hws_ng = true; toast.warn(30, 'webworkers failed to start\n\nwill be a bit slower due to\nhashing on main-thread'); apop(st.busy.hash, t); st.todo.hash.unshift(t); exec_hash(); }, 5000); for (var a = 0; a < hws.length; a++) { var w = hws[a]; w.onmessage = onmsg; if (init) w.postMessage('ping'); if (mem > 0) free.push(w); mem -= chunksize; } function go_next() { if (st.slow_io && uc.multitask) // android-chrome filereader latency is ridiculous but scales linearly // (unlike every other platform which instead suffers on parallel reads...) max_readers = opt_readers = free.length; if (reading >= max_readers || !free.length || nchunk >= nchunks) return; var w = free.pop(), car = nchunk * chunksize, cdr = Math.min(chunksize + car, t.size); //console.log('[P ] %d read bgin (%d reading, %d busy)', nchunk, reading + 1, nbusy + 1); w.postMessage([nchunk, t.fobj, car, cdr]); busy[nchunk] = w; nbusy++; reading++; nchunk++; if (Date.now() - up2k.tact > 1500) tasker(); } function go_fail() { failed = true; if (nbusy) return; apop(st.busy.hash, t); st.bytes.finished += t.size; } function onmsg(d) { if (hws_ng) return; d = d.data; var k = d[0]; if (k == "pong") if (++hws_ok == hws.length) { clearInterval(init); if (toast.tag == 'iwwt') toast.hide(); go_next(); } if (k == "panic") return vis_exh(d[1], 'up2k.js', '', '', d[1]); if (k == "fail") { var nchunk = d[1]; free.push(busy[nchunk]); delete busy[nchunk]; nbusy--; reading--; pvis.seth(t.n, 1, d[1]); pvis.seth(t.n, 2, d[2]); console.log(d[1], d[2]); if (d[1] == 'OS-error') got_oserr(); pvis.move(t.n, 'ng'); return go_fail(); } if (k == "ferr") return toast.err(0, 'y o u b r o k e i t\nfile: ' + esc(t.name + '') + '\nerror: ' + d[1]); if (k == "read") { reading--; if (MOBILE && CHROME && st.slow_io === null && d[1] == 1 && d[2] > 1024 * 512) { var spd = Math.floor(d[2] / d[3]); st.slow_io = spd < 40 * 1024; console.log('spd {0}, slow: {1}'.format(spd, st.slow_io)); } //console.log('[P ] %d read DONE (%d reading, %d busy)', d[1], reading, nbusy); return go_next(); } if (k == "done") { var nchunk = d[1], hslice = d[2], sz = d[3]; free.push(busy[nchunk]); delete busy[nchunk]; nbusy--; //console.log('[P ] %d HASH DONE (%d reading, %d busy)', nchunk, reading, nbusy); hashtab[nchunk] = buf2b64(hslice); st.bytes.hashed += sz; t.hash.push(nchunk); pvis.hashed(t); if (failed) return go_fail(); if (t.hash.length < nchunks) return nbusy < opt_readers && go_next(); t.hash = []; for (var a = 0; a < nchunks; a++) t.hash.push(hashtab[a]); t.t_hashed = Date.now(); pvis.seth(t.n, 2, L.u_hashdone); pvis.seth(t.n, 1, '📦 wait'); apop(st.busy.hash, t); st.todo.handshake.push(t); tasker(); } } if (!init) go_next(); } ///// //// /// head // function exec_head() { var t = st.todo.head.shift(); if (t.done) return console.log('done; skip head1', t.name, t); st.busy.head.push(t); var xhr = new XMLHttpRequest(); xhr.onerror = xhr.ontimeout = function () { console.log('head onerror, retrying', t.name, t); if (!toast.visible) toast.warn(9.98, L.u_enethd + "\n\nfile: " + esc(t.name), t); apop(st.busy.head, t); st.todo.head.unshift(t); }; function orz(e) { if (t.done) return console.log('done; skip head2', t.name, t); var ok = false; if (xhr.status == 200) { var srv_sz = xhr.getResponseHeader('Content-Length'), srv_ts = xhr.getResponseHeader('Last-Modified'); ok = t.size == srv_sz; if (ok && uc.datechk) { srv_ts = new Date(srv_ts) / 1000; ok = Math.abs(srv_ts - t.lmod) < 2; } } apop(st.busy.head, t); if (!ok && !t.srch) { push_t(st.todo.hash, t); tasker(); return; } t.done = true; t.fobj = null; st.bytes.hashed += t.size; st.bytes.finished += t.size; pvis.move(t.n, 'bz'); pvis.seth(t.n, 1, ok ? 'YOLO' : '404'); pvis.seth(t.n, 2, "turbo'd"); pvis.move(t.n, ok ? 'ok' : 'ng'); tasker(); }; xhr.onload = function (e) { try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); } }; xhr.open('HEAD', t.purl + uricom_enc(t.name), true); xhr.timeout = 34000; xhr.send(); } ///// //// /// handshake // function exec_handshake() { var t = st.todo.handshake.shift(), keepalive = t.keepalive, me = Date.now(); if (t.done) return console.log('done; skip hs', t.name, t); st.busy.handshake.push(t); t.keepalive = undefined; t.t_busied = me; if (keepalive) console.log("sending keepalive handshake", t.name, t); if (!t.srch && !t.t_handshake) pvis.seth(t.n, 2, L.u_hs); var xhr = new XMLHttpRequest(); xhr.onerror = xhr.ontimeout = function () { if (t.t_busied != me) // t.done ok return console.log('zombie handshake onerror', t.name, t); if (!toast.visible) toast.warn(9.98, L.u_eneths + "\n\nfile: " + esc(t.name), t); console.log('handshake onerror, retrying', t.name, t); apop(st.busy.handshake, t); st.todo.handshake.unshift(chill(t)); t.keepalive = keepalive; }; var orz = function (e) { if (t.t_busied != me || t.done) return console.log('zombie handshake onload', t.name, t); if (xhr.status == 200) { try { var response = JSON.parse(xhr.responseText); } catch (ex) { apop(st.busy.handshake, t); st.todo.handshake.unshift(chill(t)); var txt = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit; return toast.err(0, txt + '\n\n' + L.badreply + ':\n\n' + unpre(xhr.responseText)); } t.t_handshake = Date.now(); if (keepalive) { apop(st.busy.handshake, t); tasker(); return; } if (toast.tag === t) toast.ok(5, L.u_fixed); if (!response.name) { var msg = '', smsg = ''; if (!response || !response.hits || !response.hits.length) { smsg = '404'; msg = (L.u_s404 + ' (' + L.u_expl + ')'); } else { smsg = 'found'; var msg = []; for (var a = 0, aa = Math.min(20, response.hits.length); a < aa; a++) { var hit = response.hits[a], tr = unix2ui(hit.ts), tu = unix2ui(t.lmod), diff = parseInt(t.lmod) - parseInt(hit.ts), cdiff = (Math.abs(diff) <= 2) ? '3c0' : 'f0b', sdiff = 'diff ' + diff; msg.push(linksplit(hit.rp, window.up_site).join(' / ') + '
          ' + tr + ' (srv), ' + tu + ' (You), ' + sdiff + '
          '); } msg = msg.join('
          \n'); } pvis.seth(t.n, 2, msg); pvis.seth(t.n, 1, smsg); pvis.move(t.n, smsg == '404' ? 'ng' : 'ok'); apop(st.busy.handshake, t); st.bytes.finished += t.size; t.done = true; t.fobj = null; tasker(); return; } t.sprs = response.sprs; var fk = response.fk, rsp_purl = url_enc(response.purl), rename = rsp_purl !== t.purl || response.name !== t.name; if (rename || fk) { if (rename) console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + rsp_purl + "] [" + response.name + "]"); t.purl = rsp_purl; t.name = response.name; var url = t.purl + uricom_enc(t.name); if (fk) { t.fk = fk; url += '?k=' + fk; } pvis.seth(t.n, 0, linksplit(url, window.up_site).join(' / ')); } var chunksize = get_chunksize(t.size), cdr_idx = Math.ceil(t.size / chunksize) - 1, cdr_sz = (t.size % chunksize) || chunksize, cbd = []; for (var a = 0; a <= cdr_idx; a++) { cbd.push(a == cdr_idx ? cdr_sz : chunksize); } t.postlist = []; t.wark = response.wark; var missing = response.hash; for (var a = 0; a < missing.length; a++) { var idx = t.hash.indexOf(missing[a]); if (idx < 0) return modal.alert('wtf negative index for hash "{0}" in task:\n{1}'.format( missing[a], esc(JSON.stringify(t)))); t.postlist.push(idx); cbd[idx] = 0; } pvis.setat(t.n, cbd); pvis.prog(t, 0, cbd[0]); var done = true, msg = 'done'; if (t.postlist.length) { if (t.rechecks && QS('#opa_del.act')) toast.inf(30, L.u_started, L.u_unpt); var arr = st.todo.upload, sort = arr.length && arr[arr.length - 1].nfile > t.n; if (!t.stitch_sz) { // keep all connections busy var bpc = (st.bytes.total - st.bytes.finished) / (parallel_uploads || 1), ocs = 1024 * 1024, stp = 1024 * 512, ccs = ocs; while (ccs < bpc) { ocs = ccs; ccs += stp; if (ccs < bpc) ocs = ccs; ccs += stp; stp *= 2; } ocs = Math.floor(ocs / 1024 / 1024); t.stitch_sz = Math.min(ocs, stitch_tgt); } for (var a = 0; a < t.postlist.length; a++) { var nparts = [], tbytes = 0, stitch = t.stitch_sz; if (t.nojoin && t.nojoin - t.postlist.length < 6) stitch = 1; --a; for (var b = 0; b < stitch; b++) { nparts.push(t.postlist[++a]); tbytes += chunksize; if (tbytes + chunksize > stitch * 1024 * 1024 || t.postlist[a + 1] - t.postlist[a] !== 1) break; } arr.push({ 'nfile': t.n, 'nparts': nparts }); } t.nojoin = 0; msg = null; done = false; if (sort) arr.sort(function (a, b) { return a.nfile < b.nfile ? -1 : /* */ a.nfile > b.nfile ? 1 : /* */ a.nparts[0] < b.nparts[0] ? -1 : 1; }); } if (msg) pvis.seth(t.n, 1, msg); apop(st.busy.handshake, t); if (done) { t.done = true; t.fobj = null; st.bytes.finished += t.size - t.bytes_uploaded; var spd1 = (t.size / ((t.t_hashed - t.t_hashing) / 1000.)) / (1024 * 1024.), spd2 = (t.size / ((t.t_uploaded - t.t_uploading) / 1000.)) / (1024 * 1024.); pvis.seth(t.n, 2, 'hash {0}, up {1} MB/s'.format( f2f(spd1, 2), !isNum(spd2) ? '--' : f2f(spd2, 2))); pvis.move(t.n, 'ok'); } else { if (t.t_uploaded) chill(t); t.t_uploaded = undefined; } tasker(); } else { pvis.seth(t.n, 1, "ERROR"); pvis.seth(t.n, 2, L.u_ehstmp, t); apop(st.busy.handshake, t); var err = "", cls = "ERROR", rsp = unpre(xhr.responseText), ofs = rsp.lastIndexOf('\nURL: '); if (ofs !== -1) rsp = rsp.slice(0, ofs); if (rsp.indexOf('rate-limit ') !== -1) { var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0]; console.log("rate-limit: " + penalty); t.cooldown = Date.now() + parseFloat(penalty) * 1000; st.todo.handshake.unshift(t); return; } var err_pend = rsp.indexOf('partial upload exists at a different') + 1, err_srcb = rsp.indexOf('source file busy; please try again') + 1, err_plug = rsp.indexOf('upload blocked by x') + 1, err_dupe = rsp.indexOf('upload rejected, file already exists') + 1, err_exists = rsp.indexOf('upload rejected, a file with that name already exists') + 1; if (err_pend || err_srcb || err_plug || err_dupe || err_exists) { err = rsp; ofs = err.indexOf('\n/'); if (ofs !== -1) { err = err.slice(0, ofs + 1) + linksplit(err.slice(ofs + 2).trimEnd()).join(' / '); } if (!t.rechecks) t.rechecks = 0; if (t.rechecks < 999 && (err_pend || err_srcb)) { t.want_recheck = true; if (st.busy.upload.length || st.busy.handshake.length || st.bytes.uploaded) { err = L.u_dupdefer; cls = 'defer'; } } if (err_pend) { err += ' (' + L.u_expl + ')'; } } if (err != "") { if (!t.t_uploading) st.bytes.finished += t.size; pvis.seth(t.n, 1, cls); pvis.seth(t.n, 2, err); pvis.move(t.n, 'ng'); tasker(); return; } st.todo.handshake.unshift(chill(t)); if (rsp.indexOf('server HDD is full') + 1) return toast.err(0, L.u_ehsdf + "\n\n" + rsp.replace(/.*; /, '')); err = t.t_uploading ? L.u_ehsfin : t.srch ? L.u_ehssrch : L.u_ehsinit; xhrchk(xhr, err + "\n\nfile: " + esc(t.name) + "\n\nerror ", "404, target folder not found", "warn", t); } } xhr.onload = function (e) { try { orz(e); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); } }; var req = { "name": t.name, "size": t.size, "lmod": t.lmod, "life": st.lifetime, "hash": t.hash }; if (t.srch) req.srch = 1; else if (t.rand) req.rand = true; else if (t.umod) req.umod = true; if (!t.srch) { if (uc.ow == 1) req.replace = 'mt'; if (uc.ow == 2) req.replace = true; if (uc.ow == 3) req.replace = 'skip'; } xhr.open('POST', t.purl, true); xhr.responseType = 'text'; xhr.timeout = 42000 + (t.srch || t.t_uploaded ? 0 : (t.size / (1048 * 20))); // safededup 20M/s hdd xhr.send(JSON.stringify(req)); } ///// //// /// upload // function can_upload_next() { var upt = st.todo.upload[0], upf = st.files[upt.nfile], nhs = st.busy.handshake.length, hs = nhs && st.busy.handshake[0], now = Date.now(); if (nhs >= 16) return false; if (hs && hs.t_uploaded && Date.now() - hs.t_busied > 10000) // verification HS possibly held back by uploads return false; for (var a = 0, aa = st.busy.handshake.length; a < aa; a++) { var hs = st.busy.handshake[a]; if (hs.n < upt.nfile && hs.t_busied > now - 10 * 1000 && !st.files[hs.n].bytes_uploaded) return false; // handshake race; wait for lexically first } if (upf.sprs) return true; for (var a = 0, aa = st.busy.upload.length; a < aa; a++) if (st.busy.upload[a].nfile == upt.nfile) return false; return true; } function exec_upload() { var upt = st.todo.upload.shift(), t = st.files[upt.nfile], nparts = upt.nparts, pcar = nparts[0], pcdr = nparts[nparts.length - 1], maxsz = (u2sz_max > 1 ? u2sz_max : 2040) * 1024 * 1024; if (t.done) return console.log('done; skip chunk', t.name, t); st.busy.upload.push(upt); st.nfile.upload = upt.nfile; if (!t.t_uploading) t.t_uploading = Date.now(); pvis.seth(t.n, 1, "🚀 " + L.ul_send); var chunksize = get_chunksize(t.size), car = pcar * chunksize, cdr = (pcdr + 1) * chunksize; if (cdr >= t.size) cdr = t.size; if (cdr - car <= maxsz) return upload_sub(t, upt, pcar, pcdr, car, cdr, chunksize, car, []); var car0 = car, subs = []; while (car < cdr) { subs.push([car, Math.min(cdr, car + maxsz)]); car += maxsz; } upload_sub(t, upt, pcar, pcdr, 0, 0, chunksize, car0, subs); } function upload_sub(t, upt, pcar, pcdr, car, cdr, chunksize, car0, subs) { var nparts = upt.nparts, is_sub = subs.length; if (is_sub) { var x = subs.shift(); car = x[0]; cdr = x[1]; } var snpart = is_sub ? ('' + pcar + '(' + (car-car0) +'+'+ (cdr-car)) : pcar == pcdr ? pcar : ('' + pcar + '~' + pcdr); var orz = function (xhr) { st.bytes.inflight -= xhr.bsent; var txt = unpre((xhr.response && xhr.response.err) || xhr.responseText); if (txt.indexOf('upload blocked by x') + 1) { apop(st.busy.upload, upt); for (var a = pcar; a <= pcdr; a++) apop(t.postlist, a); pvis.seth(t.n, 1, "ERROR"); pvis.seth(t.n, 2, txt.split(/\n/)[0]); pvis.move(t.n, 'ng'); return; } if (xhr.status == 200) { car = car0; if (subs.length) return upload_sub(t, upt, pcar, pcdr, 0, 0, chunksize, car0, subs); var bdone = cdr - car; for (var a = pcar; a <= pcdr; a++) { pvis.prog(t, a, Math.min(bdone, chunksize)); bdone -= chunksize; } st.bytes.finished += cdr - car; st.bytes.uploaded += cdr - car; t.bytes_uploaded += cdr - car; t.cooldown = t.coolmul = 0; st.etac.u++; st.etac.t++; } else if (txt.indexOf('already got that') + 1 || txt.indexOf('already being written') + 1) { t.nojoin = t.nojoin || t.postlist.length; console.log("ignoring dupe-segment with backoff", t.nojoin, t.name, t); if (!toast.visible && st.todo.upload.length < 4) toast.inf(10, L.u_cbusy); } else { xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t); chill(t); } orz2(xhr); } var orz2 = function (xhr) { apop(st.busy.upload, upt); for (var a = pcar; a <= pcdr; a++) apop(t.postlist, a); if (!t.postlist.length) { t.t_uploaded = Date.now(); pvis.seth(t.n, 1, 'verifying'); st.todo.handshake.unshift(t); } tasker(); } function do_send() { var xhr = new XMLHttpRequest(), bfin = Math.floor(st.bytes.finished / 1024 / 1024), btot = Math.floor(st.bytes.total / 1024 / 1024); xhr.upload.onprogress = function (xev) { var nb = xev.loaded, db = nb - xhr.bsent; if (!db) return; st.bytes.inflight += db; xhr.bsent = nb; if (!IE) xhr.timeout = 64000 + Date.now() - xhr.t0; pvis.prog(t, pcar, nb); }; xhr.onload = function (xev) { try { orz(xhr); } catch (ex) { vis_exh(ex + '', 'up2k.js', '', '', ex); } }; xhr.onerror = xhr.ontimeout = function (xev) { if (crashed) return; st.bytes.inflight -= (xhr.bsent || 0); xhr.bsent = 0; if (!toast.visible) toast.warn(9.98, L.u_cuerr.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), t); t.nojoin = t.nojoin || t.postlist.length; // maybe rproxy postsize limit console.log('chunkpit onerror,', t.name, t); orz2(xhr); }; var chashes = [], ctxt = t.hash[pcar], plen = Math.floor(192 / nparts.length); plen = plen > 9 ? 9 : plen < 2 ? 2 : plen; for (var a = pcar + 1; a <= pcdr; a++) chashes.push(t.hash[a].slice(0, plen)); if (chashes.length) ctxt += ',' + plen + ',' + chashes.join(''); xhr.open('POST', t.purl, true); xhr.setRequestHeader("X-Up2k-Hash", ctxt); xhr.setRequestHeader("X-Up2k-Wark", t.wark); if (is_sub) xhr.setRequestHeader("X-Up2k-Subc", car - car0); xhr.setRequestHeader("X-Up2k-Stat", "{0}/{1}/{2}/{3} {4}/{5} {6}".format( pvis.ctr.ok, pvis.ctr.ng, pvis.ctr.bz, pvis.ctr.q, btot, btot - bfin, st.eta.t.indexOf('/s, ')+1 ? st.eta.t.split(' ').pop() : 'x')); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); if (xhr.overrideMimeType) xhr.overrideMimeType('Content-Type', 'application/octet-stream'); xhr.bsent = 0; xhr.t0 = Date.now(); xhr.timeout = 1000 * (IE ? 1234 : 42); xhr.responseType = 'text'; xhr.send(t.fobj.slice(car, cdr)); } do_send(); } ///// //// /// config ui // function onresize(e) { // 10x faster than matchMedia('(min-width var bar = ebi('ops'), wpx = window.innerWidth, fpx = parseInt(getComputedStyle(bar)['font-size']), wem = wpx * 1.0 / fpx, wide = wem > 57 ? 'w' : '', parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'), btn = ebi('u2btn'); if (btn.parentNode !== parent) { parent.appendChild(btn); ebi('u2conf').className = ebi('u2cards').className = ebi('u2etaw').className = wide; } wide = wem > 86 ? 'ww' : wide; parent = ebi(wide == 'ww' ? 'u2c3w' : 'u2c3t'); var its = [ebi('u2etaw'), ebi('u2cards')]; if (its[0].parentNode !== parent) { ebi('u2conf').className = wide; for (var a = 0; a < 2; a++) { parent.appendChild(its[a]); its[a].className = wide; } } } onresize100.add(onresize, true); if (MOBILE) { // android-chrome wobbles for a bit; firefox / iOS-safari are OK setTimeout(onresize, 20); setTimeout(onresize, 100); setTimeout(onresize, 500); } var o = QSA('#u2conf .c *[tt]'); for (var a = o.length - 1; a >= 0; a--) { o[a].parentNode.getElementsByTagName('input')[0].setAttribute('tt', o[a].getAttribute('tt')); } tt.att(QS('#u2conf')); function bumpthread2(e) { if (anymod(e)) return; var k = e.key || e.code; if (k == 'ArrowUp') bumpthread(1); if (k == 'ArrowDown') bumpthread(-1); } function bumpthread(dir) { try { dir.stopPropagation(); dir.preventDefault(); } catch (ex) { } var obj = ebi('nthread'); if (dir.target) { clmod(obj, 'err', 1); var v = Math.floor(parseInt(obj.value)); if (v < 0 || v !== v) return; if (v > 64) { var p = obj.selectionStart; v = obj.value = 64; obj.selectionStart = obj.selectionEnd = p; } parallel_uploads = v; if (v == u2j) sdrop('nthread'); else swrite('nthread', v); clmod(obj, 'err'); return; } parallel_uploads += dir; if (parallel_uploads < 0) parallel_uploads = 0; if (parallel_uploads > 16) parallel_uploads = 16; if (parallel_uploads > 6) toast.warn(10, L.u_maxconn); else if (toast.txt == L.u_maxconn) toast.hide(); obj.value = parallel_uploads; bumpthread({ "target": 1 }); } var read_u2sz = function () { var el = ebi('u2szg'), n = parseInt(el.value); stitch_tgt = n = ( isNaN(n) ? u2sz_tgt : n < u2sz_min ? u2sz_min : n > u2sz_max ? u2sz_max : n ); if (n == u2sz_tgt) sdrop('u2sz'); else swrite('u2sz', n); if (el.value != n) el.value = n; }; ebi('u2szg').addEventListener('blur', read_u2sz); ebi('u2szg').onkeydown = function (e) { if (anymod(e)) return; var k = e.key || e.code, n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0; if (!n) return; this.value = parseInt(this.value) + n; read_u2sz(); } function tgl_fsearch() { set_fsearch(!uc.fsearch); } function draw_turbo() { if (turbolvl < 0 && uc.turbo) { bcfg_set('u2turbo', uc.turbo = false); toast.err(10, L.u_turbo_c); } if (uc.turbo && !has(perms, 'read')) { bcfg_set('u2turbo', uc.turbo = false); toast.warn(30, L.u_turbo_g); } var msg = (turbolvl || !uc.turbo) ? null : uc.fsearch ? L.u_ts : L.u_tu, html = ebi('u2foot').innerHTML; if (msg && html.indexOf(msg) + 1) return; qsr('#u2turbomsg'); if (!msg) return; var o = mknod('div', 'u2turbomsg'); o.innerHTML = msg; ebi('u2foot').appendChild(o); } draw_turbo(); function draw_life() { var el = ebi('u2life'); if (!lifetime) { el.style.display = 'none'; el.innerHTML = ''; st.lifetime = 0; return; } el.style.display = uc.fsearch ? 'none' : ''; el.innerHTML = '
          ' + L.u_life_cfg + '
          ' + L.u_life_est + '
          '; set_life(Math.min(lifetime, icfg_get('lifetime', lifetime))); ebi('lifem').oninput = ebi('lifeh').oninput = mod_life; ebi('lifem').onkeydown = ebi('lifeh').onkeydown = kd_life; tt.att(ebi('u2life')); } draw_life(); function mod_life(e) { var el = e.target, pow = parseInt(el.getAttribute('p')), v = parseInt(el.value); if (!isNum(v)) return; if (toast.tag == mod_life) toast.hide(); v *= pow; if (v > lifetime) { v = lifetime; toast.warn(20, L.u_life_max.format(lhumantime(lifetime)), mod_life); } swrite('lifetime', v); set_life(v); } function kd_life(e) { var el = e.target, k = e.key || e.code, d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0; if (anymod(e) || !d) return; el.value = parseInt(el.value) + d; mod_life(e); } function set_life(v) { //ebi('lifes').value = v; ebi('lifem').value = parseInt(v / 60); ebi('lifeh').value = parseInt(v / 3600); var undo = have_unpost - (v ? lifetime - v : 0); ebi('undor').innerHTML = undo <= 0 ? L.u_unp_ng : L.u_unp_ok.format(lhumantime(undo)); st.lifetime = v; rel_life(); } function rel_life() { if (!lifetime) return; try { ebi('lifew').innerHTML = unix2ui((st.lifetime || lifetime) + Date.now() / 1000 - new Date().getTimezoneOffset() * 60 ).replace(' ', ', ').slice(0, -3); } catch (ex) { } } setInterval(rel_life, 9000); function set_potato() { pvis.potato(); set_fsearch(); } function set_fsearch(new_state) { var fixed = false, persist = new_state !== undefined, preferred = bcfg_get('fsearch', undefined), can_write = false; if (!ebi('fsearch')) { new_state = false; } else if (perms.length) { if (!(can_write = has(perms, 'write'))) { new_state = true; fixed = true; } if (!has(perms, 'read') || !have_up2k_idx) { new_state = false; fixed = true; } if (new_state === undefined && preferred === undefined) new_state = can_write ? false : have_up2k_idx ? true : undefined; } if (new_state === undefined) new_state = preferred; if (new_state !== undefined) if (persist) bcfg_set('fsearch', uc.fsearch = new_state); else bcfg_upd_ui('fsearch', uc.fsearch = new_state); try { clmod(ebi('u2c3w'), 's', !can_write); QS('label[for="fsearch"]').style.display = QS('#fsearch').style.display = fixed ? 'none' : ''; } catch (ex) { } try { var ico = uc.fsearch ? '🔎' : '🚀', desc = uc.fsearch ? L.ul_btns : L.ul_btnu; clmod(ebi('op_up2k'), 'srch', uc.fsearch); ebi('u2bm').innerHTML = ico + '  ' + desc + ''; } catch (ex) { } ebi('u2tab').className = (uc.fsearch ? 'srch ' : 'up ') + pvis.act; var potato = uc.potato && !uc.fsearch; ebi('u2cards').style.display = ebi('u2tab').style.display = potato ? 'none' : ''; ebi('u2mu').style.display = potato ? '' : 'none'; if (u2ts.startsWith('f') || !sread('u2ts')) uc.u2ts = bcfg_upd_ui('u2ts', !u2ts.endsWith('u')); draw_turbo(); draw_life(); onresize(); } function apply_flag_cfg() { if (uc.flag_en && !flag) { try { flag = up2k_flagbus(); } catch (ex) { toast.err(5, "not supported on your browser:\n" + esc(basenames(ex))); bcfg_set('flag_en', uc.flag_en = false); } } else if (!uc.flag_en && flag) { if (flag.ours) flag.give(); flag.ch.close(); flag = false; } } function set_u2sort(en) { if (u2sort.indexOf('f') < 0) return; var fen = uc.az = u2sort.indexOf('n') + 1; bcfg_upd_ui('u2sort', fen); if (en != fen) toast.warn(10, L.ul_btnlk); } function set_u2ts(en) { if (u2ts.indexOf('f') < 0) return; var fen = !u2ts.endsWith('u'); bcfg_upd_ui('u2ts', fen); if (en != fen) toast.warn(10, L.ul_btnlk); } function set_hashw() { if (!WebAssembly) { bcfg_set('hashw', uc.hashw = false); toast.err(10, L.u_nowork); } } function set_nosubtle(v) { if (!WebAssembly) return toast.err(10, L.u_nowork); modal.confirm(L.lang_set, location.reload.bind(location), null); } function set_upnag(en) { function nopenag() { bcfg_set('upnag', uc.upnag = false); toast.err(10, "https only"); } function chknag() { if (Notification.permission != 'granted') nopenag(); } if (!Notification || !HTTPS) return nopenag(); if (en && Notification.permission == 'default') Notification.requestPermission().then(chknag, chknag); set_upsfx(en); } function set_upsfx(en) { if (!en) return; toast.inf(10, 'OK -- test it!') ebi('nagtest').onclick = function () { start_actx(); uc.nagtxt = ':^)'; setTimeout(donut.enstrobe, 200); }; } if (uc.upnag && (!Notification || Notification.permission != 'granted')) bcfg_set('upnag', uc.upnag = false); ebi('nthread_add').onclick = function (e) { ev(e); bumpthread(1); }; ebi('nthread_sub').onclick = function (e) { ev(e); bumpthread(-1); }; ebi('nthread').onkeydown = bumpthread2; ebi('nthread').oninput = bumpthread; ebi('u2etas').onclick = function (e) { ev(e); clmod(ebi('u2etas'), 'o', 't'); }; set_fsearch(); bumpthread({ "target": 1 }); if (parallel_uploads < 1) bumpthread(1); setTimeout(function () { for (var a = 0; a < up2k_hooks.length; a++) up2k_hooks[a](); }, 1); return r; } function warn_uploader_busy(e) { e.preventDefault(); e.returnValue = ''; return "upload in progress, click abort and use the file-tree to navigate instead"; } tt.init(); favico.init(); ebi('ico1').onclick = function () { var a = favico.txt == this.textContent; swrite('icot', a ? 'c' : this.textContent); swrite('icof', a ? 'fc5' : '000'); swrite('icob', a ? '222' : ''); favico.init(); }; if (QS('#op_up2k.act')) goto_up2k(); apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts }); if (ls0) fileman.render(); (function () { goto(); var op = sread('opmode'); if (op !== null && op !== '.') try { goto(op); } catch (ex) { } })(); J_U2K = 2; ================================================ FILE: copyparty/web/util.js ================================================ "use strict"; var J_UTL = 1; if (!window.console || !console.log) window.console = { "log": function (msg) { } }; if (!Object.assign) Object.assign = function (a, b) { for (var k in b) a[k] = b[k]; }; if (window.CGV1) Object.assign(window, window.CGV1); if (window.CGV) Object.assign(window, window.CGV); var wah = '', STG = null, NOAC = 'autocorrect="off" autocapitalize="off"', L, tt, treectl, thegrid, up2k, asmCrypto, hashwasm, vbar, marked, T0 = Date.now(), R = SR.slice(1), RS = R ? "/" + R : "", HALFMAX = 8192 * 8192 * 8192 * 8192, HTTPS = ('' + location).indexOf('https:') === 0, TOUCH = 'ontouchstart' in window, MOBILE = TOUCH, CHROME = !!window.chrome, // safari=false VCHROME = CHROME ? 1 : 0, UA = '' + navigator.userAgent, IE = !!document.documentMode, FIREFOX = ('netscape' in window) && / rv:/.test(UA), IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(UA), LINUX = /Linux/.test(UA), MACOS = /Macintosh/.test(UA), WINDOWS = /Windows/.test(UA), APPLE = IPHONE || MACOS, APPLEM = TOUCH && APPLE; if (!window.WebAssembly || !WebAssembly.Memory) window.WebAssembly = false; if (!window.Notification || !Notification.permission) window.Notification = false; if (!window.FormData) window.FormData = false; try { STG = window.localStorage; STG.STG; } catch (ex) { STG = null; if ((ex + '').indexOf('sandbox') < 0) console.log('no localStorage: ' + ex); } try { if (navigator.userAgentData.mobile) MOBILE = true; if (navigator.userAgentData.platform == 'Windows') WINDOWS = true; CHROME = navigator.userAgentData.brands.find(function (d) { return d.brand == 'Chromium' }); if (CHROME) VCHROME = parseInt(CHROME.version); else VCHROME = 0; CHROME = !!CHROME; } catch (ex) { } var ebi = document.getElementById.bind(document), QS = document.querySelector.bind(document), QSA = document.querySelectorAll.bind(document), XHR = XMLHttpRequest; function mknod(et, eid, html) { var ret = document.createElement(et); if (eid) ret.id = eid; if (html) ret.innerHTML = html; return ret; } function qsr(sel) { var el = QS(sel); if (el) el.parentNode.removeChild(el); return el; } // error handler for mobile devices function esc(txt) { return txt.replace(/[&"<>]/g, function (c) { return { '&': '&', '"': '"', '<': '<', '>': '>' }[c]; }); } function basenames(txt) { return (txt + '').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js'); } if ((location + '').indexOf(',rej,') + 1) window.onunhandledrejection = function (e) { var err = e.reason; try { err += '\n' + e.reason.stack; } catch (e) { } err = basenames(err); console.log("REJ: " + err); try { toast.warn(30, err); } catch (e) { } }; try { console.hist = []; var CMAXHIST = MOBILE ? 9000 : 44000; var hook = function (t) { var orig = console[t].bind(console), cfun = function () { console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', ')); if (console.hist.length > CMAXHIST) console.hist = console.hist.slice(CMAXHIST / 4); orig.apply(console, arguments); }; console['std' + t] = orig; console[t] = cfun; }; hook('log'); console.log('log-capture ok'); hook('debug'); hook('warn'); hook('error'); } catch (ex) { if (console.stdlog) console.log = console.stdlog; console.log('console capture failed', ex); } var crashed = false, ignexd = {}, evalex_fatal = false; function vis_exh(msg, url, lineNo, columnNo, error) { var ekey = url + '\n' + lineNo + '\n' + msg; if (ignexd[ekey] || crashed) return; msg = String(msg); url = String(url); if (msg.indexOf('ResizeObserver') + 1) return; // chrome issue 809574 (benign, from